From 4629a91db261aa15df27a84a10bc361a0fb83ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Ver=C5=A1i=C4=87?= Date: Mon, 10 Jun 2024 19:33:33 +0200 Subject: [PATCH] feat!: implement built-in vs custom on-chain parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marin Veršić --- Cargo.lock | 1 + cli/src/lib.rs | 3 +- cli/src/samples.rs | 3 +- client/benches/tps/utils.rs | 16 +- client/examples/register_1000_triggers.rs | 35 +- client/examples/tutorial.rs | 8 +- client/src/client.rs | 17 +- client/src/config/user.rs | 8 +- client/tests/integration/add_domain.rs | 37 - client/tests/integration/asset.rs | 6 +- client/tests/integration/asset_propagation.rs | 14 +- client/tests/integration/events/data.rs | 2 +- client/tests/integration/events/pipeline.rs | 22 +- .../integration/extra_functional/genesis.rs | 2 +- .../multiple_blocks_created.rs | 14 +- .../integration/extra_functional/normal.rs | 21 +- .../extra_functional/unregister_peer.rs | 19 +- .../extra_functional/unstable_network.rs | 15 +- client/tests/integration/mod.rs | 1 - client/tests/integration/non_mintable.rs | 4 +- .../integration/queries/smart_contract.rs | 12 +- client/tests/integration/set_parameter.rs | 66 +- .../integration/smartcontracts/Cargo.toml | 1 + .../src/lib.rs | 8 +- .../executor_custom_data_model/Cargo.toml | 3 + .../executor_custom_data_model/src/lib.rs | 1 + .../src/parameters.rs | 6 +- .../src/lib.rs | 2 +- .../src/lib.rs | 2 +- .../executor_remove_permission/src/lib.rs | 2 +- .../executor_with_admin/src/lib.rs | 2 +- .../executor_with_custom_parameter/Cargo.toml | 25 + .../executor_with_custom_parameter/src/lib.rs | 51 ++ .../executor_with_migration_fail/src/lib.rs | 2 +- client/tests/integration/sorting.rs | 60 +- .../integration/triggers/time_trigger.rs | 33 +- client/tests/integration/tx_history.rs | 2 +- client/tests/integration/upgrade.rs | 80 ++- .../assets/test_register_asset_definitions.py | 18 - .../test/domains/test_register_domains.py | 24 - client_cli/src/main.rs | 31 +- config/src/parameters/actual.rs | 75 +- config/src/parameters/defaults.rs | 40 +- config/src/parameters/user.rs | 100 +-- config/tests/fixtures.rs | 49 +- config/tests/fixtures/full.toml | 21 +- configs/peer.template.toml | 4 +- configs/swarm/executor.wasm | Bin 508130 -> 515745 bytes configs/swarm/genesis.json | 42 -- core/benches/blocks/common.rs | 13 +- core/benches/kura.rs | 11 +- core/benches/validation.rs | 13 +- core/src/block.rs | 38 +- core/src/block_sync.rs | 6 +- core/src/executor.rs | 9 +- core/src/gossiper.rs | 30 +- core/src/lib.rs | 4 - core/src/query/store.rs | 6 +- core/src/queue.rs | 58 +- core/src/smartcontracts/isi/account.rs | 19 +- core/src/smartcontracts/isi/asset.rs | 9 +- core/src/smartcontracts/isi/domain.rs | 23 +- core/src/smartcontracts/isi/mod.rs | 3 +- core/src/smartcontracts/isi/query.rs | 43 +- core/src/smartcontracts/isi/triggers/mod.rs | 15 +- .../isi/triggers/specialized.rs | 2 +- core/src/smartcontracts/isi/world.rs | 119 +-- core/src/smartcontracts/wasm.rs | 46 +- core/src/state.rs | 237 ++---- core/src/sumeragi/main_loop.rs | 136 ++-- core/src/sumeragi/mod.rs | 3 - core/src/tx.rs | 49 +- core/test_network/src/lib.rs | 9 +- data_model/derive/src/id.rs | 22 +- .../derive/tests/has_origin_generics.rs | 6 - data_model/src/account.rs | 18 +- data_model/src/asset.rs | 6 +- data_model/src/block.rs | 13 +- data_model/src/domain.rs | 3 +- data_model/src/events/data/events.rs | 86 ++- data_model/src/events/data/filters.rs | 16 - data_model/src/events/pipeline.rs | 24 +- data_model/src/executor.rs | 31 +- data_model/src/ipfs.rs | 7 +- data_model/src/isi.rs | 61 +- data_model/src/lib.rs | 476 +----------- data_model/src/metadata.rs | 225 +----- data_model/src/name.rs | 30 +- data_model/src/parameter.rs | 680 ++++++++++++++++++ data_model/src/peer.rs | 11 +- data_model/src/permission.rs | 13 +- data_model/src/query/cursor.rs | 4 +- data_model/src/query/mod.rs | 49 +- data_model/src/query/predicate.rs | 2 +- data_model/src/role.rs | 16 +- data_model/src/transaction.rs | 50 +- data_model/src/trigger.rs | 6 +- data_model/src/visit.rs | 5 - default_executor/src/lib.rs | 2 +- docs/source/references/schema.json | 510 +++++++------ ffi/src/std_impls.rs | 16 +- primitives/src/json.rs | 15 +- schema/gen/src/lib.rs | 19 +- smart_contract/executor/derive/src/default.rs | 1 - smart_contract/executor/derive/src/lib.rs | 11 + .../executor/derive/src/parameter.rs | 43 ++ smart_contract/executor/src/default.rs | 24 +- smart_contract/executor/src/lib.rs | 43 +- smart_contract/executor/src/parameter.rs | 17 + smart_contract/executor/src/permission.rs | 6 +- tools/kagami/src/genesis/generate.rs | 74 +- tools/parity_scale_cli/samples/trigger.bin | Bin 131 -> 131 bytes tools/parity_scale_cli/src/main.rs | 11 +- torii/src/routing.rs | 2 +- 114 files changed, 2047 insertions(+), 2518 deletions(-) delete mode 100644 client/tests/integration/add_domain.rs create mode 100644 client/tests/integration/smartcontracts/executor_with_custom_parameter/Cargo.toml create mode 100644 client/tests/integration/smartcontracts/executor_with_custom_parameter/src/lib.rs create mode 100644 data_model/src/parameter.rs create mode 100644 smart_contract/executor/derive/src/parameter.rs create mode 100644 smart_contract/executor/src/parameter.rs diff --git a/Cargo.lock b/Cargo.lock index 027e85a9751..921bd1d5b87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1599,6 +1599,7 @@ name = "executor_custom_data_model" version = "2.0.0-pre-rc.21" dependencies = [ "iroha_data_model", + "iroha_executor", "iroha_schema", "serde", "serde_json", diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 0f03eda9c36..ded6b11fbe0 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -294,8 +294,7 @@ impl Iroha { None } }.unwrap_or_else(|| { - State::from_config( - config.chain_wide, + State::new( world, Arc::clone(&kura), live_query_store_handle.clone(), diff --git a/cli/src/samples.rs b/cli/src/samples.rs index 5aaa54701a5..b03d96279bd 100644 --- a/cli/src/samples.rs +++ b/cli/src/samples.rs @@ -67,9 +67,8 @@ pub fn get_config_toml( .write(["sumeragi", "trusted_peers"], peers) .write(["network", "address"], DEFAULT_P2P_ADDR) .write(["network", "block_gossip_period_ms"], 500) - .write(["network", "block_gossip_max_size"], 1) + .write(["network", "block_gossip_size"], 1) .write(["torii", "address"], DEFAULT_TORII_ADDR) - .write(["chain_wide", "max_transactions_in_block"], 2) .write(["genesis", "public_key"], genesis_public_key) .write( ["genesis", "signed_file"], diff --git a/client/benches/tps/utils.rs b/client/benches/tps/utils.rs index 6fe35e80d3f..cca409724ae 100644 --- a/client/benches/tps/utils.rs +++ b/client/benches/tps/utils.rs @@ -6,7 +6,7 @@ use iroha::{ crypto::KeyPair, data_model::{ events::pipeline::{BlockEventFilter, BlockStatus}, - parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, + parameter::BlockParameter, prelude::*, }, }; @@ -22,7 +22,7 @@ pub struct Config { pub peers: u32, /// Interval in microseconds between transactions to reduce load pub interval_us_per_tx: u64, - pub max_txs_per_block: u32, + pub block_limits: BlockParameter, pub blocks: u32, pub sample_size: u32, pub genesis_max_retries: u32, @@ -33,11 +33,7 @@ impl fmt::Display for Config { write!( f, "{}peers-{}interval_µs-{}max_txs-{}blocks-{}samples", - self.peers, - self.interval_us_per_tx, - self.max_txs_per_block, - self.blocks, - self.sample_size, + self.peers, self.interval_us_per_tx, self.block_limits, self.blocks, self.sample_size, ) } } @@ -55,11 +51,7 @@ impl Config { let clients = network.clients(); wait_for_genesis_committed_with_max_retries(&clients, 0, self.genesis_max_retries); - client.submit_all_blocking( - ParametersBuilder::new() - .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, self.max_txs_per_block)? - .into_set_parameters(), - )?; + client.submit_blocking(SetParameter::new(Parameter::Block(self.block_limits)))?; let unit_names = (UnitName::MIN..).take(self.peers as usize); let units = clients diff --git a/client/examples/register_1000_triggers.rs b/client/examples/register_1000_triggers.rs index 567dd9d3317..a7f31bd2962 100644 --- a/client/examples/register_1000_triggers.rs +++ b/client/examples/register_1000_triggers.rs @@ -1,10 +1,14 @@ //! Example of registering multiple triggers //! Used to show Iroha's trigger deduplication capabilities +use std::num::NonZeroU64; + use iroha::{ client::Client, + crypto::KeyPair, data_model::{prelude::*, trigger::TriggerId}, }; +use iroha_data_model::parameter::{Parameter, SmartContractParameter}; use iroha_genesis::{GenesisBlock, GenesisBuilder}; use iroha_primitives::unique_vec; use irohad::samples::{construct_executor, get_config}; @@ -18,17 +22,24 @@ use tokio::runtime::Runtime; fn generate_genesis( num_triggers: u32, chain_id: ChainId, - genesis_key_pair: &iroha_crypto::KeyPair, + genesis_key_pair: &KeyPair, topology: Vec, ) -> Result> { - let builder = GenesisBuilder::default(); + let builder = GenesisBuilder::default() + .append_instruction(SetParameter::new(Parameter::Executor( + SmartContractParameter::Fuel(NonZeroU64::MAX), + ))) + .append_instruction(SetParameter::new(Parameter::Executor( + SmartContractParameter::Memory(NonZeroU64::MAX), + ))); - let wasm = - iroha_wasm_builder::Builder::new("tests/integration/smartcontracts/mint_rose_trigger") - .show_output() - .build()? - .optimize()? - .into_bytes()?; + let wasm = iroha_wasm_builder::Builder::new( + "client/tests/integration/smartcontracts/mint_rose_trigger", + ) + .show_output() + .build()? + .optimize()? + .into_bytes()?; let wasm = WasmSmartContract::from_compiled(wasm); let (account_id, _account_keypair) = gen_account_in("wonderland"); @@ -54,7 +65,7 @@ fn generate_genesis( }) .fold(builder, GenesisBuilder::append_instruction); - let executor = construct_executor("../default_executor").expect("Failed to construct executor"); + let executor = construct_executor("default_executor").expect("Failed to construct executor"); Ok(builder.build_and_sign(executor, chain_id, genesis_key_pair, topology)) } @@ -64,17 +75,13 @@ fn main() -> Result<(), Box> { let chain_id = get_chain_id(); let genesis_key_pair = get_key_pair(test_network::Signatory::Genesis); let topology = vec![peer.id.clone()]; - let mut configuration = get_config( + let configuration = get_config( unique_vec![peer.id.clone()], chain_id.clone(), get_key_pair(test_network::Signatory::Peer), genesis_key_pair.public_key(), ); - // Increase executor limits for large genesis - configuration.chain_wide.executor_runtime.fuel_limit = u64::MAX; - configuration.chain_wide.executor_runtime.max_memory = u32::MAX.into(); - let genesis = generate_genesis(1_000_u32, chain_id, &genesis_key_pair, topology)?; let builder = PeerBuilder::new() diff --git a/client/examples/tutorial.rs b/client/examples/tutorial.rs index 1589b8d78ad..4718137ab0c 100644 --- a/client/examples/tutorial.rs +++ b/client/examples/tutorial.rs @@ -34,7 +34,7 @@ fn domain_registration_test(config: Config) -> Result<(), Error> { use iroha::{ client::Client, data_model::{ - metadata::UnlimitedMetadata, + metadata::Metadata, prelude::{Domain, DomainId, InstructionBox, Register}, }, }; @@ -57,7 +57,7 @@ fn domain_registration_test(config: Config) -> Result<(), Error> { // #region domain_register_example_prepare_tx // Prepare a transaction - let metadata = UnlimitedMetadata::default(); + let metadata = Metadata::default(); let instructions: Vec = vec![create_looking_glass.into()]; let tx = iroha.build_transaction(instructions, metadata); // #endregion domain_register_example_prepare_tx @@ -101,7 +101,7 @@ fn account_registration_test(config: Config) -> Result<(), Error> { client::Client, crypto::KeyPair, data_model::{ - metadata::UnlimitedMetadata, + metadata::Metadata, prelude::{Account, AccountId, InstructionBox, Register}, }, }; @@ -127,7 +127,7 @@ fn account_registration_test(config: Config) -> Result<(), Error> { // #region register_account_prepare_tx // Prepare a transaction using the // Account's RegisterBox - let metadata = UnlimitedMetadata::new(); + let metadata = Metadata::default(); let instructions: Vec = vec![create_account.into()]; let tx = iroha.build_transaction(instructions, metadata); // #endregion register_account_prepare_tx diff --git a/client/src/client.rs b/client/src/client.rs index cb88d6d0d11..624f454d82c 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -332,6 +332,7 @@ impl_query_output! { crate::data_model::executor::ExecutorDataModel, crate::data_model::trigger::Trigger, crate::data_model::prelude::Numeric, + crate::data_model::parameter::Parameters, } /// Iroha client @@ -453,7 +454,7 @@ impl Client { pub fn build_transaction( &self, instructions: impl Into, - metadata: UnlimitedMetadata, + metadata: Metadata, ) -> SignedTransaction { let tx_builder = TransactionBuilder::new(self.chain.clone(), self.account.clone()); @@ -510,7 +511,7 @@ impl Client { &self, instructions: impl IntoIterator, ) -> Result> { - self.submit_all_with_metadata(instructions, UnlimitedMetadata::new()) + self.submit_all_with_metadata(instructions, Metadata::default()) } /// Instructions API entry point. Submits one Iroha Special Instruction to `Iroha` peers. @@ -522,7 +523,7 @@ impl Client { pub fn submit_with_metadata( &self, instruction: impl Instruction, - metadata: UnlimitedMetadata, + metadata: Metadata, ) -> Result> { self.submit_all_with_metadata([instruction], metadata) } @@ -536,7 +537,7 @@ impl Client { pub fn submit_all_with_metadata( &self, instructions: impl IntoIterator, - metadata: UnlimitedMetadata, + metadata: Metadata, ) -> Result> { self.submit_transaction(&self.build_transaction(instructions, metadata)) } @@ -719,7 +720,7 @@ impl Client { &self, instructions: impl IntoIterator, ) -> Result> { - self.submit_all_blocking_with_metadata(instructions, UnlimitedMetadata::new()) + self.submit_all_blocking_with_metadata(instructions, Metadata::default()) } /// Submits and waits until the transaction is either rejected or committed. @@ -731,7 +732,7 @@ impl Client { pub fn submit_blocking_with_metadata( &self, instruction: impl Instruction, - metadata: UnlimitedMetadata, + metadata: Metadata, ) -> Result> { self.submit_all_blocking_with_metadata(vec![instruction.into()], metadata) } @@ -745,7 +746,7 @@ impl Client { pub fn submit_all_blocking_with_metadata( &self, instructions: impl IntoIterator, - metadata: UnlimitedMetadata, + metadata: Metadata, ) -> Result> { let transaction = self.build_transaction(instructions, metadata); self.submit_transaction_blocking(&transaction) @@ -1621,7 +1622,7 @@ mod tests { }); let build_transaction = - || client.build_transaction(Vec::::new(), UnlimitedMetadata::new()); + || client.build_transaction(Vec::::new(), Metadata::default()); let tx1 = build_transaction(); let tx2 = build_transaction(); assert_ne!(tx1.hash(), tx2.hash()); diff --git a/client/src/config/user.rs b/client/src/config/user.rs index 000ab2a2dd8..71bf826d4d3 100644 --- a/client/src/config/user.rs +++ b/client/src/config/user.rs @@ -6,11 +6,13 @@ use iroha_config_base::{ util::{DurationMs, Emitter, EmitterResultExt}, ReadConfig, WithOrigin, }; -use iroha_crypto::{KeyPair, PrivateKey, PublicKey}; -use iroha_data_model::prelude::{AccountId, ChainId, DomainId}; use url::Url; -use crate::config::BasicAuth; +use crate::{ + config::BasicAuth, + crypto::{KeyPair, PrivateKey, PublicKey}, + data_model::prelude::{AccountId, ChainId, DomainId}, +}; /// Root of the user configuration #[derive(Clone, Debug, ReadConfig)] diff --git a/client/tests/integration/add_domain.rs b/client/tests/integration/add_domain.rs deleted file mode 100644 index 514e18b85d6..00000000000 --- a/client/tests/integration/add_domain.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::thread; - -use eyre::Result; -use iroha::{client, data_model::prelude::*}; -use iroha_config::parameters::actual::Root as Config; -use test_network::*; - -#[test] -// This test suite is also covered at the UI level in the iroha_cli tests -// in test_register_domains.py -fn client_add_domain_with_name_length_more_than_limit_should_not_commit_transaction() -> Result<()> -{ - 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 = Config::pipeline_time(); - - // Given - - let normal_domain_id: DomainId = "sora".parse()?; - let create_domain = Register::domain(Domain::new(normal_domain_id.clone())); - test_client.submit(create_domain)?; - - let too_long_domain_name: DomainId = "0".repeat(2_usize.pow(14)).parse()?; - let create_domain = Register::domain(Domain::new(too_long_domain_name.clone())); - test_client.submit(create_domain)?; - - thread::sleep(pipeline_time * 2); - - assert!(test_client - .request(client::domain::by_id(normal_domain_id)) - .is_ok()); - assert!(test_client - .request(client::domain::by_id(too_long_domain_name)) - .is_err()); - - Ok(()) -} diff --git a/client/tests/integration/asset.rs b/client/tests/integration/asset.rs index f0fe33e8fd9..390b472b659 100644 --- a/client/tests/integration/asset.rs +++ b/client/tests/integration/asset.rs @@ -106,7 +106,7 @@ fn client_add_asset_quantity_to_existing_asset_should_increase_asset_amount() -> let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); let create_asset = Register::asset_definition(AssetDefinition::numeric(asset_definition_id.clone())); - let metadata = iroha::data_model::metadata::UnlimitedMetadata::default(); + let metadata = iroha::data_model::metadata::Metadata::default(); //When let quantity = numeric!(200); let mint = Mint::asset_numeric( @@ -137,7 +137,7 @@ fn client_add_big_asset_quantity_to_existing_asset_should_increase_asset_amount( let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); let create_asset = Register::asset_definition(AssetDefinition::numeric(asset_definition_id.clone())); - let metadata = iroha::data_model::metadata::UnlimitedMetadata::default(); + let metadata = iroha::data_model::metadata::Metadata::default(); //When let quantity = Numeric::new(2_u128.pow(65), 0); let mint = Mint::asset_numeric( @@ -168,7 +168,7 @@ fn client_add_asset_with_decimal_should_increase_asset_amount() -> Result<()> { let asset_definition_id = AssetDefinitionId::from_str("xor#wonderland").expect("Valid"); let asset_definition = AssetDefinition::numeric(asset_definition_id.clone()); let create_asset = Register::asset_definition(asset_definition); - let metadata = iroha::data_model::metadata::UnlimitedMetadata::default(); + let metadata = iroha::data_model::metadata::Metadata::default(); //When let quantity = numeric!(123.456); diff --git a/client/tests/integration/asset_propagation.rs b/client/tests/integration/asset_propagation.rs index bcf99a5ca9c..8e6984ac0ca 100644 --- a/client/tests/integration/asset_propagation.rs +++ b/client/tests/integration/asset_propagation.rs @@ -3,12 +3,10 @@ use std::{str::FromStr as _, thread}; use eyre::Result; use iroha::{ client::{self, QueryResult}, - data_model::{ - parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, - prelude::*, - }, + data_model::{parameter::BlockParameter, prelude::*}, }; use iroha_config::parameters::actual::Root as Config; +use nonzero_ext::nonzero; use test_network::*; use test_samples::gen_account_in; @@ -22,11 +20,9 @@ fn client_add_asset_quantity_to_existing_asset_should_increase_asset_amount_on_a wait_for_genesis_committed(&network.clients(), 0); let pipeline_time = Config::pipeline_time(); - client.submit_all_blocking( - ParametersBuilder::new() - .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? - .into_set_parameters(), - )?; + client.submit_blocking(SetParameter::new(Parameter::Block( + BlockParameter::MaxTransactions(nonzero!(1_u64)), + )))?; let create_domain: InstructionBox = Register::domain(Domain::new(DomainId::from_str("domain")?)).into(); diff --git a/client/tests/integration/events/data.rs b/client/tests/integration/events/data.rs index 1e35a9819ed..e1c8d12b987 100644 --- a/client/tests/integration/events/data.rs +++ b/client/tests/integration/events/data.rs @@ -151,7 +151,7 @@ fn transaction_execution_should_produce_events( // submit transaction to produce events init_receiver.recv()?; - let transaction = client.build_transaction(executable, UnlimitedMetadata::new()); + let transaction = client.build_transaction(executable, Metadata::default()); client.submit_transaction_blocking(&transaction)?; // assertion diff --git a/client/tests/integration/events/pipeline.rs b/client/tests/integration/events/pipeline.rs index f9b0817c252..f57d4382563 100644 --- a/client/tests/integration/events/pipeline.rs +++ b/client/tests/integration/events/pipeline.rs @@ -1,7 +1,4 @@ -use std::{ - num::NonZeroUsize, - thread::{self, JoinHandle}, -}; +use std::thread::{self, JoinHandle}; use eyre::Result; use iroha::{ @@ -11,7 +8,7 @@ use iroha::{ BlockEvent, BlockEventFilter, BlockStatus, TransactionEventFilter, TransactionStatus, }, isi::error::InstructionExecutionError, - parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, + parameter::BlockParameter, prelude::*, transaction::error::TransactionRejectionReason, ValidationFail, @@ -19,6 +16,7 @@ use iroha::{ }; use iroha_config::parameters::actual::Root as Config; use iroha_data_model::query::error::FindError; +use nonzero_ext::nonzero; use test_network::*; // Needed to re-enable ignored tests. @@ -59,15 +57,13 @@ fn test_with_instruction_and_status_and_port( wait_for_genesis_committed(&clients, 0); let pipeline_time = Config::pipeline_time(); - client.submit_all_blocking( - ParametersBuilder::new() - .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? - .into_set_parameters(), - )?; + client.submit_blocking(SetParameter::new(Parameter::Block( + BlockParameter::MaxTransactions(nonzero!(1_u64)), + )))?; // Given let submitter = client; - let transaction = submitter.build_transaction(instruction, UnlimitedMetadata::new()); + let transaction = submitter.build_transaction(instruction, Metadata::default()); let hash = transaction.hash(); let mut handles = Vec::new(); for listener in clients { @@ -133,8 +129,6 @@ fn applied_block_must_be_available_in_kura() { .as_ref() .expect("Must be some") .kura() - .get_block_by_height( - NonZeroUsize::new(event.header().height().try_into().unwrap()).unwrap(), - ) + .get_block_by_height(event.header().height().try_into().unwrap()) .expect("Block applied event was received earlier"); } diff --git a/client/tests/integration/extra_functional/genesis.rs b/client/tests/integration/extra_functional/genesis.rs index f0c0507e497..eb2da99b843 100644 --- a/client/tests/integration/extra_functional/genesis.rs +++ b/client/tests/integration/extra_functional/genesis.rs @@ -1,4 +1,4 @@ -use iroha_data_model::{ +use iroha::data_model::{ domain::{Domain, DomainId}, isi::Register, }; diff --git a/client/tests/integration/extra_functional/multiple_blocks_created.rs b/client/tests/integration/extra_functional/multiple_blocks_created.rs index 458af606d10..f48fbf521f5 100644 --- a/client/tests/integration/extra_functional/multiple_blocks_created.rs +++ b/client/tests/integration/extra_functional/multiple_blocks_created.rs @@ -3,12 +3,10 @@ use std::thread; use eyre::Result; use iroha::{ client::{self, Client, QueryResult}, - data_model::{ - parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, - prelude::*, - }, + data_model::{parameter::BlockParameter, prelude::*}, }; use iroha_config::parameters::actual::Root as Config; +use nonzero_ext::nonzero; use test_network::*; use test_samples::gen_account_in; @@ -22,11 +20,9 @@ fn long_multiple_blocks_created() -> Result<()> { wait_for_genesis_committed(&network.clients(), 0); let pipeline_time = Config::pipeline_time(); - client.submit_all_blocking( - ParametersBuilder::new() - .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? - .into_set_parameters(), - )?; + client.submit_blocking(SetParameter::new(Parameter::Block( + BlockParameter::MaxTransactions(nonzero!(1_u64)), + )))?; let create_domain: InstructionBox = Register::domain(Domain::new("domain".parse()?)).into(); let (account_id, _account_keypair) = gen_account_in("domain"); diff --git a/client/tests/integration/extra_functional/normal.rs b/client/tests/integration/extra_functional/normal.rs index c4a2c930ee4..401d3b22626 100644 --- a/client/tests/integration/extra_functional/normal.rs +++ b/client/tests/integration/extra_functional/normal.rs @@ -1,18 +1,19 @@ -use std::num::NonZeroU32; - -use iroha::client; -use iroha_config::parameters::actual::Root as Config; -use iroha_data_model::{asset::AssetDefinitionId, prelude::*}; +use iroha::{ + client, + data_model::{asset::AssetDefinitionId, parameter::BlockParameter, prelude::*}, +}; +use nonzero_ext::nonzero; use test_network::*; #[test] fn tranasctions_should_be_applied() { - let mut configuration = Config::test(); - configuration.chain_wide.max_transactions_in_block = NonZeroU32::new(1).unwrap(); - let (_rt, network, iroha) = NetworkBuilder::new(4, Some(11_300)) - .with_config(configuration) - .create_with_runtime(); + let (_rt, network, iroha) = NetworkBuilder::new(4, Some(11_300)).create_with_runtime(); wait_for_genesis_committed(&network.clients(), 0); + iroha + .submit_blocking(SetParameter::new(Parameter::Block( + BlockParameter::MaxTransactions(nonzero!(1_u64)), + ))) + .unwrap(); let domain_id = "and".parse::().unwrap(); let account_id = "ed01201F803CB23B1AAFB958368DF2F67CB78A2D1DFB47FFFC3133718F165F54DFF677@and" diff --git a/client/tests/integration/extra_functional/unregister_peer.rs b/client/tests/integration/extra_functional/unregister_peer.rs index ade2324d525..5fa97a5f231 100644 --- a/client/tests/integration/extra_functional/unregister_peer.rs +++ b/client/tests/integration/extra_functional/unregister_peer.rs @@ -3,12 +3,10 @@ use std::thread; use eyre::Result; use iroha::{ client::{self, QueryResult}, - data_model::{ - parameter::{default::MAX_TRANSACTIONS_IN_BLOCK, ParametersBuilder}, - prelude::*, - }, + data_model::{parameter::BlockParameter, prelude::*}, }; use iroha_config::parameters::actual::Root as Config; +use nonzero_ext::nonzero; use test_network::*; use test_samples::gen_account_in; @@ -117,20 +115,23 @@ fn init() -> Result<( let (rt, network, client) = Network::start_test_with_runtime(4, Some(10_925)); let pipeline_time = Config::pipeline_time(); iroha_logger::info!("Started"); - let parameters = ParametersBuilder::new() - .add_parameter(MAX_TRANSACTIONS_IN_BLOCK, 1u32)? - .into_set_parameters(); + + let set_max_txns_in_block = SetParameter::new(Parameter::Block( + BlockParameter::MaxTransactions(nonzero!(1_u64)), + )); + let create_domain = Register::domain(Domain::new("domain".parse()?)); let (account_id, _account_keypair) = gen_account_in("domain"); let create_account = Register::account(Account::new(account_id.clone())); let asset_definition_id: AssetDefinitionId = "xor#domain".parse()?; let create_asset = Register::asset_definition(AssetDefinition::numeric(asset_definition_id.clone())); - let instructions = parameters.into_iter().chain([ + let instructions: [InstructionBox; 4] = [ + set_max_txns_in_block.into(), create_domain.into(), create_account.into(), create_asset.into(), - ]); + ]; client.submit_all_blocking(instructions)?; iroha_logger::info!("Init"); Ok(( diff --git a/client/tests/integration/extra_functional/unstable_network.rs b/client/tests/integration/extra_functional/unstable_network.rs index a1c3d46328e..c917b85580d 100644 --- a/client/tests/integration/extra_functional/unstable_network.rs +++ b/client/tests/integration/extra_functional/unstable_network.rs @@ -2,15 +2,17 @@ use std::thread; use iroha::{ client::{self, QueryResult}, - data_model::prelude::*, + data_model::{ + parameter::{BlockParameter, Parameter}, + prelude::*, + }, }; use iroha_config::parameters::actual::Root as Config; +use nonzero_ext::nonzero; use rand::seq::SliceRandom; use test_network::*; use test_samples::ALICE_ID; -const MAX_TRANSACTIONS_IN_BLOCK: u32 = 5; - #[test] fn unstable_network_5_peers_1_fault() { let n_peers = 4; @@ -51,8 +53,6 @@ fn unstable_network( // Given let mut configuration = Config::test(); - configuration.chain_wide.max_transactions_in_block = - MAX_TRANSACTIONS_IN_BLOCK.try_into().unwrap(); #[cfg(debug_assertions)] { configuration.sumeragi.debug_force_soft_fork = force_soft_fork; @@ -63,6 +63,11 @@ fn unstable_network( .with_offline_peers(0) .create_with_runtime(); wait_for_genesis_committed(&network.clients(), n_offline_peers); + iroha + .submit_blocking(SetParameter::new(Parameter::Block( + BlockParameter::MaxTransactions(nonzero!(5_u64)), + ))) + .unwrap(); let pipeline_time = Config::pipeline_time(); diff --git a/client/tests/integration/mod.rs b/client/tests/integration/mod.rs index 37299969665..13b8bd2528c 100644 --- a/client/tests/integration/mod.rs +++ b/client/tests/integration/mod.rs @@ -1,4 +1,3 @@ -mod add_domain; mod asset; mod asset_propagation; mod domain_owner_permissions; diff --git a/client/tests/integration/non_mintable.rs b/client/tests/integration/non_mintable.rs index 4f65579a9be..d976fce1eb9 100644 --- a/client/tests/integration/non_mintable.rs +++ b/client/tests/integration/non_mintable.rs @@ -3,7 +3,7 @@ use std::str::FromStr as _; use eyre::Result; use iroha::{ client::{self, QueryResult}, - data_model::{isi::InstructionBox, metadata::UnlimitedMetadata, prelude::*}, + data_model::{isi::InstructionBox, prelude::*}, }; use test_network::*; use test_samples::ALICE_ID; @@ -20,7 +20,7 @@ fn non_mintable_asset_can_be_minted_once_but_not_twice() -> Result<()> { AssetDefinition::numeric(asset_definition_id.clone()).mintable_once(), ); - let metadata = UnlimitedMetadata::default(); + let metadata = Metadata::default(); let mint = Mint::asset_numeric( 200_u32, diff --git a/client/tests/integration/queries/smart_contract.rs b/client/tests/integration/queries/smart_contract.rs index 551949f2bae..e41c4bd985a 100644 --- a/client/tests/integration/queries/smart_contract.rs +++ b/client/tests/integration/queries/smart_contract.rs @@ -20,10 +20,8 @@ fn live_query_is_dropped_after_smart_contract_end() -> Result<()> { .optimize()? .into_bytes()?; - let transaction = client.build_transaction( - WasmSmartContract::from_compiled(wasm), - UnlimitedMetadata::default(), - ); + let transaction = + client.build_transaction(WasmSmartContract::from_compiled(wasm), Metadata::default()); client.submit_transaction_blocking(&transaction)?; let metadata_value: JsonString = client.request(FindAccountKeyValueByIdAndKey::new( @@ -59,10 +57,8 @@ fn smart_contract_can_filter_queries() -> Result<()> { .optimize()? .into_bytes()?; - let transaction = client.build_transaction( - WasmSmartContract::from_compiled(wasm), - UnlimitedMetadata::default(), - ); + let transaction = + client.build_transaction(WasmSmartContract::from_compiled(wasm), Metadata::default()); client.submit_transaction_blocking(&transaction)?; Ok(()) diff --git a/client/tests/integration/set_parameter.rs b/client/tests/integration/set_parameter.rs index cf2fa11accb..ec376d3f817 100644 --- a/client/tests/integration/set_parameter.rs +++ b/client/tests/integration/set_parameter.rs @@ -1,9 +1,12 @@ -use std::str::FromStr; +use std::time::Duration; use eyre::Result; use iroha::{ - client::{self, QueryResult}, - data_model::prelude::*, + client, + data_model::{ + parameter::{Parameter, Parameters, SumeragiParameter, SumeragiParameters}, + prelude::*, + }, }; use test_network::*; @@ -12,51 +15,22 @@ fn can_change_parameter_value() -> Result<()> { let (_rt, _peer, test_client) = ::new().with_port(11_135).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); - let parameter = Parameter::from_str("?BlockTime=4000")?; - let parameter_id = ParameterId::from_str("BlockTime")?; - let param_box = SetParameter::new(parameter); + let old_params: Parameters = test_client.request(client::parameter::all())?; + assert_eq!( + old_params.sumeragi().block_time(), + SumeragiParameters::default().block_time() + ); - let old_params = test_client - .request(client::parameter::all())? - .collect::>>()?; - let param_val_old = old_params - .iter() - .find(|param| param.id() == ¶meter_id) - .expect("Parameter should exist") - .val(); + let block_time = 40_000; + let parameter = Parameter::Sumeragi(SumeragiParameter::BlockTimeMs(block_time)); + let set_param_isi = SetParameter::new(parameter); + test_client.submit_blocking(set_param_isi)?; - test_client.submit_blocking(param_box)?; + let sumeragi_params = test_client.request(client::parameter::all())?.sumeragi; + assert_eq!( + sumeragi_params.block_time(), + Duration::from_millis(block_time) + ); - let new_params = test_client - .request(client::parameter::all())? - .collect::>>()?; - let param_val_new = new_params - .iter() - .find(|param| param.id() == ¶meter_id) - .expect("Parameter should exist") - .val(); - - assert_ne!(param_val_old, param_val_new); - Ok(()) -} - -#[test] -fn parameter_propagated() -> Result<()> { - let (_rt, _peer, test_client) = ::new().with_port(10_985).start_with_runtime(); - wait_for_genesis_committed(&vec![test_client.clone()], 0); - - let too_long_domain_name: DomainId = "0".repeat(2_usize.pow(8)).parse()?; - let create_domain = Register::domain(Domain::new(too_long_domain_name)); - let _ = test_client - .submit_blocking(create_domain.clone()) - .expect_err("Should fail before ident length limits update"); - - let parameter = Parameter::from_str("?WSVIdentLengthLimits=1,256_LL")?; - let param_box = SetParameter::new(parameter); - test_client.submit_blocking(param_box)?; - - test_client - .submit_blocking(create_domain) - .expect("Should work after ident length limits update"); Ok(()) } diff --git a/client/tests/integration/smartcontracts/Cargo.toml b/client/tests/integration/smartcontracts/Cargo.toml index 5004748a0c0..e6fb9bcaf40 100644 --- a/client/tests/integration/smartcontracts/Cargo.toml +++ b/client/tests/integration/smartcontracts/Cargo.toml @@ -13,6 +13,7 @@ members = [ "mint_rose_trigger", "executor_with_admin", "executor_with_custom_permission", + "executor_with_custom_parameter", "executor_remove_permission", "executor_with_migration_fail", "executor_custom_instructions_simple", diff --git a/client/tests/integration/smartcontracts/create_nft_for_every_user_trigger/src/lib.rs b/client/tests/integration/smartcontracts/create_nft_for_every_user_trigger/src/lib.rs index 811e64fbbb8..83fb83970b0 100644 --- a/client/tests/integration/smartcontracts/create_nft_for_every_user_trigger/src/lib.rs +++ b/client/tests/integration/smartcontracts/create_nft_for_every_user_trigger/src/lib.rs @@ -21,8 +21,6 @@ fn main(_id: TriggerId, _owner: AccountId, _event: EventBox) { let accounts_cursor = FindAllAccounts.execute().dbg_unwrap(); - let limits = MetadataLimits::new(256, 256); - let bad_domain_ids: [DomainId; 2] = [ "genesis".parse().dbg_unwrap(), "garden_of_live_flowers".parse().dbg_unwrap(), @@ -35,7 +33,7 @@ fn main(_id: TriggerId, _owner: AccountId, _event: EventBox) { continue; } - let mut metadata = Metadata::new(); + let mut metadata = Metadata::default(); let name = format!( "nft_for_{}_in_{}", account.id().signatory(), @@ -43,14 +41,14 @@ fn main(_id: TriggerId, _owner: AccountId, _event: EventBox) { ) .parse() .dbg_unwrap(); - metadata.insert_with_limits(name, true, limits).dbg_unwrap(); + metadata.insert(name, true); let nft_id = generate_new_nft_id(account.id()); let nft_definition = AssetDefinition::store(nft_id.clone()) .mintable_once() .with_metadata(metadata); let account_nft_id = AssetId::new(nft_id, account.id().clone()); - let account_nft = Asset::new(account_nft_id, Metadata::new()); + let account_nft = Asset::new(account_nft_id, Metadata::default()); Register::asset_definition(nft_definition) .execute() diff --git a/client/tests/integration/smartcontracts/executor_custom_data_model/Cargo.toml b/client/tests/integration/smartcontracts/executor_custom_data_model/Cargo.toml index 6ce6deb6833..be8ba50c7f6 100644 --- a/client/tests/integration/smartcontracts/executor_custom_data_model/Cargo.toml +++ b/client/tests/integration/smartcontracts/executor_custom_data_model/Cargo.toml @@ -8,6 +8,9 @@ authors.workspace = true license.workspace = true [dependencies] +# TODO: Cargo complains if I take it from workspace +iroha_executor = { version = "=2.0.0-pre-rc.21", path = "../../../../../smart_contract/executor", features = ["debug"] } + iroha_data_model.workspace = true iroha_schema.workspace = true diff --git a/client/tests/integration/smartcontracts/executor_custom_data_model/src/lib.rs b/client/tests/integration/smartcontracts/executor_custom_data_model/src/lib.rs index 08da8f82822..d245d6f5290 100644 --- a/client/tests/integration/smartcontracts/executor_custom_data_model/src/lib.rs +++ b/client/tests/integration/smartcontracts/executor_custom_data_model/src/lib.rs @@ -5,4 +5,5 @@ extern crate alloc; pub mod complex_isi; +pub mod parameters; pub mod simple_isi; diff --git a/client/tests/integration/smartcontracts/executor_custom_data_model/src/parameters.rs b/client/tests/integration/smartcontracts/executor_custom_data_model/src/parameters.rs index 9c7bb31cfdd..621e646a963 100644 --- a/client/tests/integration/smartcontracts/executor_custom_data_model/src/parameters.rs +++ b/client/tests/integration/smartcontracts/executor_custom_data_model/src/parameters.rs @@ -1,14 +1,12 @@ //! Module with custom parameters use alloc::{format, string::String, vec::Vec}; +use iroha_executor::prelude::*; use iroha_schema::IntoSchema; -use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; -use iroha_executor::prelude::*; - /// Parameter that controls domain limits -#[derive(PartialEq, Eq, Parameter, Decode, Encode, Serialize, Deserialize, IntoSchema)] +#[derive(PartialEq, Eq, Parameter, Serialize, Deserialize, IntoSchema)] pub struct DomainLimits { /// Length of domain id in bytes pub id_len: u32, diff --git a/client/tests/integration/smartcontracts/executor_custom_instructions_complex/src/lib.rs b/client/tests/integration/smartcontracts/executor_custom_instructions_complex/src/lib.rs index 7bd80e4e8d1..9c7ab1f2f2f 100644 --- a/client/tests/integration/smartcontracts/executor_custom_instructions_complex/src/lib.rs +++ b/client/tests/integration/smartcontracts/executor_custom_instructions_complex/src/lib.rs @@ -77,7 +77,7 @@ impl executor_custom_data_model::complex_isi::Context for Context { } #[entrypoint] -pub fn migrate(_block_height: u64) -> MigrationResult { +fn migrate(_block_height: u64) -> MigrationResult { DataModelBuilder::with_default_permissions() .with_custom_instruction::() .build_and_set(); diff --git a/client/tests/integration/smartcontracts/executor_custom_instructions_simple/src/lib.rs b/client/tests/integration/smartcontracts/executor_custom_instructions_simple/src/lib.rs index 425f3a16f19..27f1cf2cecd 100644 --- a/client/tests/integration/smartcontracts/executor_custom_instructions_simple/src/lib.rs +++ b/client/tests/integration/smartcontracts/executor_custom_instructions_simple/src/lib.rs @@ -55,7 +55,7 @@ fn execute_mint_asset_for_all_accounts(isi: MintAssetForAllAccounts) -> Result<( } #[entrypoint] -pub fn migrate(_block_height: u64) -> MigrationResult { +fn migrate(_block_height: u64) -> MigrationResult { DataModelBuilder::with_default_permissions() .with_custom_instruction::() .build_and_set(); diff --git a/client/tests/integration/smartcontracts/executor_remove_permission/src/lib.rs b/client/tests/integration/smartcontracts/executor_remove_permission/src/lib.rs index a88a34fd123..83583d2cec0 100644 --- a/client/tests/integration/smartcontracts/executor_remove_permission/src/lib.rs +++ b/client/tests/integration/smartcontracts/executor_remove_permission/src/lib.rs @@ -23,7 +23,7 @@ struct Executor { } #[entrypoint] -pub fn migrate(_block_height: u64) -> MigrationResult { +fn migrate(_block_height: u64) -> MigrationResult { // Note that actually migration will reset token schema to default (minus `CanUnregisterDomain`) // So any added custom permission tokens will be also removed DataModelBuilder::with_default_permissions() diff --git a/client/tests/integration/smartcontracts/executor_with_admin/src/lib.rs b/client/tests/integration/smartcontracts/executor_with_admin/src/lib.rs index f34d4f2eb57..0f6b152a16b 100644 --- a/client/tests/integration/smartcontracts/executor_with_admin/src/lib.rs +++ b/client/tests/integration/smartcontracts/executor_with_admin/src/lib.rs @@ -32,6 +32,6 @@ fn visit_instruction(executor: &mut Executor, authority: &AccountId, isi: &Instr } #[entrypoint] -pub fn migrate(_block_height: u64) -> MigrationResult { +fn migrate(_block_height: u64) -> MigrationResult { Ok(()) } diff --git a/client/tests/integration/smartcontracts/executor_with_custom_parameter/Cargo.toml b/client/tests/integration/smartcontracts/executor_with_custom_parameter/Cargo.toml new file mode 100644 index 00000000000..fe61791b90e --- /dev/null +++ b/client/tests/integration/smartcontracts/executor_with_custom_parameter/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "executor_with_custom_parameter" + +edition.workspace = true +version.workspace = true +authors.workspace = true + +license.workspace = true + +[lib] +crate-type = ['cdylib'] + +[dependencies] +executor_custom_data_model.workspace = true +iroha_executor.workspace = true +iroha_schema.workspace = true + +parity-scale-codec.workspace = true +anyhow.workspace = true +serde_json.workspace = true +serde.workspace = true + +panic-halt.workspace = true +lol_alloc.workspace = true +getrandom.workspace = true diff --git a/client/tests/integration/smartcontracts/executor_with_custom_parameter/src/lib.rs b/client/tests/integration/smartcontracts/executor_with_custom_parameter/src/lib.rs new file mode 100644 index 00000000000..401ce4a17cf --- /dev/null +++ b/client/tests/integration/smartcontracts/executor_with_custom_parameter/src/lib.rs @@ -0,0 +1,51 @@ +//! Runtime Executor which allows domains whose id satisfies the length limit +#![no_std] + +extern crate alloc; +#[cfg(not(test))] +extern crate panic_halt; + +use alloc::format; + +use executor_custom_data_model::parameters::DomainLimits; +use iroha_executor::{prelude::*, DataModelBuilder}; +use lol_alloc::{FreeListAllocator, LockedAllocator}; + +#[global_allocator] +static ALLOC: LockedAllocator = LockedAllocator::new(FreeListAllocator::new()); + +getrandom::register_custom_getrandom!(iroha_executor::stub_getrandom); + +#[derive(Constructor, ValidateEntrypoints, Validate, Visit)] +#[visit(custom(visit_register_domain))] +struct Executor { + verdict: Result, + block_height: u64, +} + +fn visit_register_domain(executor: &mut Executor, _authority: &AccountId, isi: &Register) { + let parameters = FindAllParameters.execute().unwrap().into_inner(); + + let domain_limits: DomainLimits = parameters + .custom() + .get(&DomainLimits::id()) + .unwrap() + .try_into() + .expect("INTERNAL BUG: Failed to deserialize json as `DomainLimits`"); + + iroha_executor::log::info!(&format!("Registering domain: {}", isi.object().id())); + if isi.object().id().name().as_ref().len() > domain_limits.id_len as usize { + deny!(executor, "Domain id exceeds the limit"); + } + + execute!(executor, isi); +} + +#[entrypoint] +fn migrate(_block_height: u64) -> MigrationResult { + DataModelBuilder::with_default_permissions() + .add_parameter(DomainLimits::default()) + .build_and_set(); + + Ok(()) +} diff --git a/client/tests/integration/smartcontracts/executor_with_migration_fail/src/lib.rs b/client/tests/integration/smartcontracts/executor_with_migration_fail/src/lib.rs index 0aaa7907707..98d517d084c 100644 --- a/client/tests/integration/smartcontracts/executor_with_migration_fail/src/lib.rs +++ b/client/tests/integration/smartcontracts/executor_with_migration_fail/src/lib.rs @@ -24,7 +24,7 @@ struct Executor { } #[entrypoint] -pub fn migrate(_block_height: u64) -> MigrationResult { +fn migrate(_block_height: u64) -> MigrationResult { // Performing side-effects to check in the test that it won't be applied after failure // Registering a new domain (using ISI) diff --git a/client/tests/integration/sorting.rs b/client/tests/integration/sorting.rs index dd95bba5579..490a371ebef 100644 --- a/client/tests/integration/sorting.rs +++ b/client/tests/integration/sorting.rs @@ -51,14 +51,8 @@ fn correct_pagination_assets_after_creating_new_one() { let asset_definition_id = AssetDefinitionId::from_str(&format!("xor{i}#wonderland")).expect("Valid"); let asset_definition = AssetDefinition::store(asset_definition_id.clone()); - let mut asset_metadata = Metadata::new(); - asset_metadata - .insert_with_limits( - sort_by_metadata_key.clone(), - i as u32, - MetadataLimits::new(10, 23), - ) - .expect("Valid"); + let mut asset_metadata = Metadata::default(); + asset_metadata.insert(sort_by_metadata_key.clone(), i as u32); let asset = Asset::new( AssetId::new(asset_definition_id, account_id.clone()), AssetValue::Store(asset_metadata), @@ -146,14 +140,8 @@ fn correct_sorting_of_entities() { for i in 0..n { let asset_definition_id = AssetDefinitionId::from_str(&format!("xor_{i}#wonderland")).expect("Valid"); - let mut asset_metadata = Metadata::new(); - asset_metadata - .insert_with_limits( - sort_by_metadata_key.clone(), - n - i - 1, - MetadataLimits::new(10, 28), - ) - .expect("Valid"); + let mut asset_metadata = Metadata::default(); + asset_metadata.insert(sort_by_metadata_key.clone(), n - i - 1); let asset_definition = AssetDefinition::numeric(asset_definition_id.clone()) .with_metadata(asset_metadata.clone()); @@ -207,14 +195,8 @@ fn correct_sorting_of_entities() { public_keys.sort_unstable(); for i in 0..n { let account_id = AccountId::new(domain_id.clone(), public_keys[i as usize].clone()); - let mut account_metadata = Metadata::new(); - account_metadata - .insert_with_limits( - sort_by_metadata_key.clone(), - n - i - 1, - MetadataLimits::new(10, 28), - ) - .expect("Valid"); + let mut account_metadata = Metadata::default(); + account_metadata.insert(sort_by_metadata_key.clone(), n - i - 1); let account = Account::new(account_id.clone()).with_metadata(account_metadata.clone()); accounts.push(account_id); @@ -255,14 +237,8 @@ fn correct_sorting_of_entities() { let n = 10u32; for i in 0..n { let domain_id = DomainId::from_str(&format!("neverland{i}")).expect("Valid"); - let mut domain_metadata = Metadata::new(); - domain_metadata - .insert_with_limits( - sort_by_metadata_key.clone(), - n - i - 1, - MetadataLimits::new(10, 28), - ) - .expect("Valid"); + let mut domain_metadata = Metadata::default(); + domain_metadata.insert(sort_by_metadata_key.clone(), n - i - 1); let domain = Domain::new(domain_id.clone()).with_metadata(domain_metadata.clone()); domains.push(domain_id); @@ -302,14 +278,8 @@ fn correct_sorting_of_entities() { let mut instructions = vec![]; for (idx, val) in input { let domain_id = DomainId::from_str(&format!("neverland_{idx}")).expect("Valid"); - let mut domain_metadata = Metadata::new(); - domain_metadata - .insert_with_limits( - sort_by_metadata_key.clone(), - val, - MetadataLimits::new(10, 28), - ) - .expect("Valid"); + let mut domain_metadata = Metadata::default(); + domain_metadata.insert(sort_by_metadata_key.clone(), val); let domain = Domain::new(domain_id.clone()).with_metadata(domain_metadata.clone()); domains.push(domain_id); @@ -376,14 +346,8 @@ fn sort_only_elements_which_have_sorting_key() -> Result<()> { accounts_b.push(account_id); account } else { - let mut account_metadata = Metadata::new(); - account_metadata - .insert_with_limits( - sort_by_metadata_key.clone(), - n - i - 1, - MetadataLimits::new(10, 28), - ) - .expect("Valid"); + let mut account_metadata = Metadata::default(); + account_metadata.insert(sort_by_metadata_key.clone(), n - i - 1); let account = Account::new(account_id.clone()).with_metadata(account_metadata); accounts_a.push(account_id); account diff --git a/client/tests/integration/triggers/time_trigger.rs b/client/tests/integration/triggers/time_trigger.rs index f26b2f2ff45..c77ca97eea9 100644 --- a/client/tests/integration/triggers/time_trigger.rs +++ b/client/tests/integration/triggers/time_trigger.rs @@ -6,16 +6,31 @@ use iroha::{ data_model::{ asset::AssetId, events::pipeline::{BlockEventFilter, BlockStatus}, + parameter::SumeragiParameters, prelude::*, transaction::WasmSmartContract, Level, }, }; -use iroha_config::parameters::defaults::chain_wide::CONSENSUS_ESTIMATION as DEFAULT_CONSENSUS_ESTIMATION; use iroha_logger::info; use test_network::*; use test_samples::{gen_account_in, ALICE_ID}; +/// Default estimation of consensus duration. +pub fn default_consensus_estimation() -> Duration { + let default_parameters = SumeragiParameters::default(); + + default_parameters + .block_time() + .checked_add( + default_parameters + .commit_time() + .checked_div(2) + .map_or_else(|| unreachable!(), |x| x), + ) + .map_or_else(|| unreachable!(), |x| x) +} + fn curr_time() -> core::time::Duration { use std::time::SystemTime; @@ -41,7 +56,7 @@ macro_rules! const_assert { fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result<()> { const PERIOD: Duration = Duration::from_millis(100); const ACCEPTABLE_ERROR_PERCENT: u8 = 15; - const_assert!(PERIOD.as_millis() < DEFAULT_CONSENSUS_ESTIMATION.as_millis()); + 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(); @@ -77,7 +92,7 @@ fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result Duration::from_secs(1), 3, )?; - std::thread::sleep(DEFAULT_CONSENSUS_ESTIMATION); + std::thread::sleep(default_consensus_estimation()); let finish_time = curr_time(); let average_count = finish_time.saturating_sub(start_time).as_millis() / PERIOD.as_millis(); @@ -104,7 +119,7 @@ fn mint_asset_after_3_sec() -> Result<()> { let (_rt, _peer, test_client) = ::new().with_port(10_665).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); // Sleep to certainly bypass time interval analyzed by genesis - std::thread::sleep(DEFAULT_CONSENSUS_ESTIMATION); + std::thread::sleep(default_consensus_estimation()); let asset_definition_id = "rose#wonderland" .parse::() @@ -139,7 +154,7 @@ fn mint_asset_after_3_sec() -> Result<()> { assert_eq!(init_quantity, after_registration_quantity); // Sleep long enough that trigger start is in the past - std::thread::sleep(DEFAULT_CONSENSUS_ESTIMATION); + std::thread::sleep(default_consensus_estimation()); test_client.submit_blocking(Log::new(Level::DEBUG, "Just to create block".to_string()))?; let after_wait_quantity = test_client.request(FindAssetQuantityById { @@ -201,9 +216,10 @@ fn pre_commit_trigger_should_be_executed() -> Result<()> { #[test] fn mint_nft_for_every_user_every_1_sec() -> Result<()> { - // Building trigger - info!("Building trigger"); + const TRIGGER_PERIOD: Duration = Duration::from_millis(1000); + const EXPECTED_COUNT: u64 = 4; + info!("Building trigger"); let wasm = iroha_wasm_builder::Builder::new( "tests/integration/smartcontracts/create_nft_for_every_user_trigger", ) @@ -214,9 +230,6 @@ fn mint_nft_for_every_user_every_1_sec() -> Result<()> { info!("WASM size is {} bytes", wasm.len()); - const TRIGGER_PERIOD: Duration = Duration::from_millis(1000); - const EXPECTED_COUNT: u64 = 4; - let (_rt, _peer, mut test_client) = ::new().with_port(10_780).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); diff --git a/client/tests/integration/tx_history.rs b/client/tests/integration/tx_history.rs index 77ee2fcfa2e..be415f90eae 100644 --- a/client/tests/integration/tx_history.rs +++ b/client/tests/integration/tx_history.rs @@ -46,7 +46,7 @@ fn client_has_rejected_and_acepted_txs_should_return_tx_history() -> Result<()> &mint_not_existed_asset }; let instructions: Vec = vec![mint_asset.clone().into()]; - let transaction = client.build_transaction(instructions, UnlimitedMetadata::new()); + let transaction = client.build_transaction(instructions, Metadata::default()); client.submit_transaction(&transaction)?; } thread::sleep(pipeline_time * 5); diff --git a/client/tests/integration/upgrade.rs b/client/tests/integration/upgrade.rs index b55e228edd0..7ca8996ef31 100644 --- a/client/tests/integration/upgrade.rs +++ b/client/tests/integration/upgrade.rs @@ -4,14 +4,16 @@ use eyre::Result; use futures_util::TryStreamExt as _; use iroha::{ client::{self, Client, QueryResult}, - data_model::prelude::*, + data_model::{ + parameter::{Parameter, SmartContractParameter}, + prelude::*, + }, }; -use iroha_data_model::parameter::{default::EXECUTOR_FUEL_LIMIT, ParametersBuilder}; use iroha_logger::info; +use nonzero_ext::nonzero; use serde_json::json; use test_network::*; use test_samples::{ALICE_ID, BOB_ID}; -use tokio::sync::mpsc; const ADMIN_PUBLIC_KEY_MULTIHASH: &str = "ed012076E5CA9698296AF9BE2CA45F525CB3BCFDEB7EE068BA56F973E9DD90564EF4FC"; @@ -240,25 +242,14 @@ fn executor_custom_instructions_complex() -> Result<()> { use executor_custom_data_model::complex_isi::{ ConditionalExpr, CoreExpr, EvaluatesTo, Expression, Greater, }; - use iroha_config::parameters::actual::Root as Config; - let mut config = Config::test(); - // Note that this value will be overwritten by genesis block with NewParameter ISI - // But it will be needed after NewParameter removal in #4597 - config.chain_wide.executor_runtime.fuel_limit = 1_000_000_000; - - let (_rt, _peer, client) = PeerBuilder::new() - .with_port(11_275) - .with_config(config) - .start_with_runtime(); + let (_rt, _peer, client) = PeerBuilder::new().with_port(11_275).start_with_runtime(); wait_for_genesis_committed(&vec![client.clone()], 0); - // Remove this after #4597 - config value will be used (see above) - let parameters = ParametersBuilder::new() - .add_parameter(EXECUTOR_FUEL_LIMIT, Numeric::from(1_000_000_000_u32))? - .into_set_parameters(); - client.submit_all_blocking(parameters)?; - + let executor_fuel_limit = SetParameter::new(Parameter::Executor(SmartContractParameter::Fuel( + nonzero!(1_000_000_000_u64), + ))); + client.submit_blocking(executor_fuel_limit)?; upgrade_executor( &client, "tests/integration/smartcontracts/executor_custom_instructions_complex", @@ -344,10 +335,8 @@ fn migration_should_cause_upgrade_event() { let (rt, _peer, client) = ::new().with_port(10_996).start_with_runtime(); wait_for_genesis_committed(&vec![client.clone()], 0); - let (sender, mut receiver) = mpsc::channel(1); let events_client = client.clone(); - - let _handle = rt.spawn(async move { + let task = rt.spawn(async move { let mut stream = events_client .listen_for_events_async([ExecutorEventFilter::new()]) .await @@ -357,7 +346,8 @@ fn migration_should_cause_upgrade_event() { new_data_model, }))) = event { - let _ = sender.send(new_data_model).await; + assert!(!new_data_model.permissions.is_empty()); + break; } } }); @@ -368,15 +358,43 @@ fn migration_should_cause_upgrade_event() { ) .unwrap(); - let data_model = rt - .block_on(async { - tokio::time::timeout(std::time::Duration::from_secs(60), receiver.recv()).await - }) - .ok() - .flatten() - .expect("should receive upgraded event immediately after upgrade"); + rt.block_on(async { + tokio::time::timeout(core::time::Duration::from_secs(60), task) + .await + .unwrap() + }) + .expect("should receive upgraded event immediately after upgrade"); +} + +#[test] +fn define_custom_parameter() -> Result<()> { + use executor_custom_data_model::parameters::DomainLimits; + + let (_rt, _peer, client) = ::new().with_port(10_996).start_with_runtime(); + wait_for_genesis_committed(&vec![client.clone()], 0); - assert!(!data_model.permissions.is_empty()); + let long_domain_name = "0".repeat(2_usize.pow(5)).parse::()?; + let create_domain = Register::domain(Domain::new(long_domain_name)); + client.submit_blocking(create_domain)?; + + upgrade_executor( + &client, + "tests/integration/smartcontracts/executor_with_custom_parameter", + ) + .unwrap(); + + let too_long_domain_name = "1".repeat(2_usize.pow(5)).parse::()?; + let create_domain = Register::domain(Domain::new(too_long_domain_name)); + let _err = client.submit_blocking(create_domain.clone()).unwrap_err(); + + let parameter = DomainLimits { + id_len: 2_u32.pow(6), + } + .into(); + let set_param_isi: InstructionBox = SetParameter::new(parameter).into(); + client.submit_all_blocking([set_param_isi, create_domain.into()])?; + + Ok(()) } fn upgrade_executor(client: &Client, executor: impl AsRef) -> Result<()> { diff --git a/client_cli/pytests/test/assets/test_register_asset_definitions.py b/client_cli/pytests/test/assets/test_register_asset_definitions.py index 03233ccc373..69216a078fb 100644 --- a/client_cli/pytests/test/assets/test_register_asset_definitions.py +++ b/client_cli/pytests/test/assets/test_register_asset_definitions.py @@ -33,24 +33,6 @@ def test_register_asset_definition_with_numeric_value_type( ) -@allure.label("sdk_test_id", "register_asset_definition_with_too_long_name") -def test_register_asset_definition_with_too_long_name( - GIVEN_129_length_name, GIVEN_registered_domain, GIVEN_numeric_value_type -): - with allure.step( - f'WHEN client_cli registers the asset_definition "{GIVEN_129_length_name}" ' - f'with "{GIVEN_numeric_value_type}" value type' - f'in the "{GIVEN_registered_domain.name}" domain' - ): - client_cli.register().asset().definition( - asset=GIVEN_129_length_name, - domain=GIVEN_registered_domain.name, - value_type=GIVEN_numeric_value_type, - ) - with allure.step(f'THEN Iroha should have the asset "{GIVEN_129_length_name}"'): - client_cli.should(have.error(Stderr.TOO_LONG.value)) - - @allure.label("sdk_test_id", "register_asset_definition_with_store_value_type") def test_register_asset_definition_with_store_value_type( GIVEN_fake_asset_name, GIVEN_registered_domain, GIVEN_store_value_type diff --git a/client_cli/pytests/test/domains/test_register_domains.py b/client_cli/pytests/test/domains/test_register_domains.py index 7f01073934e..4b9752c9b5d 100644 --- a/client_cli/pytests/test/domains/test_register_domains.py +++ b/client_cli/pytests/test/domains/test_register_domains.py @@ -66,30 +66,6 @@ def test_register_one_letter_domain(GIVEN_random_character): iroha.should(have.domain(GIVEN_random_character)) -@allure.label("sdk_test_id", "register_max_length_domain") -def test_register_max_length_domain(GIVEN_128_length_name): - with allure.step( - f'WHEN client_cli registers the longest domain "{GIVEN_128_length_name}"' - ): - client_cli.register().domain(GIVEN_128_length_name) - with allure.step( - f'THEN Iroha should have the longest domain "{GIVEN_128_length_name}"' - ): - iroha.should(have.domain(GIVEN_128_length_name)) - - -@allure.label("sdk_test_id", "register_domain_with_too_long_name") -def test_register_domain_with_too_long_name(GIVEN_129_length_name): - with allure.step( - f'WHEN client_cli registers the domain "{GIVEN_129_length_name}" with too long name' - ): - client_cli.register().domain(GIVEN_129_length_name) - with allure.step( - f'THEN client_cli should have the too long domain error: "{Stderr.TOO_LONG}"' - ): - client_cli.should(have.error(Stderr.TOO_LONG.value)) - - @allure.label("sdk_test_id", "register_domain_with_reserved_character") def test_register_domain_with_reserved_character(GIVEN_string_with_reserved_character): with allure.step( diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index c4d25cc2039..78955ac62b1 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -30,20 +30,19 @@ pub struct MetadataArgs { } impl MetadataArgs { - fn load(self) -> Result { - let value: Option = self + fn load(self) -> Result { + let value: Option = self .metadata .map(|path| { let content = fs::read_to_string(&path).wrap_err_with(|| { eyre!("Failed to read the metadata file `{}`", path.display()) })?; - let metadata: UnlimitedMetadata = - json5::from_str(&content).wrap_err_with(|| { - eyre!( - "Failed to deserialize metadata from file `{}`", - path.display() - ) - })?; + let metadata: Metadata = json5::from_str(&content).wrap_err_with(|| { + eyre!( + "Failed to deserialize metadata from file `{}`", + path.display() + ) + })?; Ok::<_, eyre::Report>(metadata) }) .transpose()?; @@ -235,7 +234,7 @@ fn color_mode() -> ColorMode { #[allow(clippy::shadow_unrelated)] fn submit( instructions: impl Into, - metadata: UnlimitedMetadata, + metadata: Metadata, context: &mut dyn RunContext, ) -> Result<()> { let iroha = context.client_from_config(); @@ -488,7 +487,7 @@ mod domain { value: MetadataValueArg { value }, } = self; let set_key_value = SetKeyValue::domain(id, key, value); - submit([set_key_value], UnlimitedMetadata::new(), context) + submit([set_key_value], Metadata::default(), context) .wrap_err("Failed to submit Set instruction") } } @@ -508,7 +507,7 @@ mod domain { fn run(self, context: &mut dyn RunContext) -> Result<()> { let Self { id, key } = self; let remove_key_value = RemoveKeyValue::domain(id, key); - submit([remove_key_value], UnlimitedMetadata::new(), context) + submit([remove_key_value], Metadata::default(), context) .wrap_err("Failed to submit Remove instruction") } } @@ -885,7 +884,7 @@ mod asset { } = self; let set = iroha::data_model::isi::SetKeyValue::asset(asset_id, key, value); - submit([set], UnlimitedMetadata::default(), context)?; + submit([set], Metadata::default(), context)?; Ok(()) } } @@ -903,7 +902,7 @@ mod asset { fn run(self, context: &mut dyn RunContext) -> Result<()> { let Self { asset_id, key } = self; let remove = iroha::data_model::isi::RemoveKeyValue::asset(asset_id, key); - submit([remove], UnlimitedMetadata::default(), context)?; + submit([remove], Metadata::default(), context)?; Ok(()) } } @@ -1034,7 +1033,7 @@ mod wasm { submit( WasmSmartContract::from_compiled(raw_data), - UnlimitedMetadata::new(), + Metadata::default(), context, ) .wrap_err("Failed to submit a Wasm smart contract") @@ -1074,7 +1073,7 @@ mod json { match self.variant { Variant::Transaction => { let instructions: Vec = json5::from_str(&string_content)?; - submit(instructions, UnlimitedMetadata::new(), context) + submit(instructions, Metadata::default(), context) .wrap_err("Failed to submit parsed instructions") } Variant::Query => { diff --git a/config/src/parameters/actual.rs b/config/src/parameters/actual.rs index 0bea7dd1b13..8e51f814c44 100644 --- a/config/src/parameters/actual.rs +++ b/config/src/parameters/actual.rs @@ -10,12 +10,8 @@ use std::{ use error_stack::{Result, ResultExt}; use iroha_config_base::{read::ConfigReader, toml::TomlSource, util::Bytes, WithOrigin}; use iroha_crypto::{KeyPair, PublicKey}; -use iroha_data_model::{ - metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, ChainId, - LengthLimits, -}; +use iroha_data_model::{peer::PeerId, ChainId}; use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; -use serde::{Deserialize, Serialize}; use url::Url; pub use user::{DevTelemetry, Logger, Snapshot}; @@ -42,7 +38,6 @@ pub struct Root { pub snapshot: Snapshot, pub telemetry: Option, pub dev_telemetry: DevTelemetry, - pub chain_wide: ChainWide, } /// See [`Root::from_toml_source`] @@ -168,78 +163,14 @@ impl Default for LiveQueryStore { #[derive(Debug, Clone, Copy)] pub struct BlockSync { pub gossip_period: Duration, - pub gossip_max_size: NonZeroU32, + pub gossip_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 domain_metadata_limits: MetadataLimits, - pub asset_definition_metadata_limits: MetadataLimits, - pub account_metadata_limits: MetadataLimits, - pub asset_metadata_limits: MetadataLimits, - pub trigger_metadata_limits: MetadataLimits, - pub ident_length_limits: LengthLimits, - pub executor_runtime: WasmRuntime, - 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 - } - - /// Estimates as `block_time + commit_time / 2` - pub fn consensus_estimation(&self) -> Duration { - self.block_time + (self.commit_time / 2) - } -} - -impl Default for ChainWide { - fn default() -> Self { - Self { - max_transactions_in_block: defaults::chain_wide::MAX_TXS, - block_time: defaults::chain_wide::BLOCK_TIME, - commit_time: defaults::chain_wide::COMMIT_TIME, - transaction_limits: defaults::chain_wide::TRANSACTION_LIMITS, - domain_metadata_limits: defaults::chain_wide::METADATA_LIMITS, - account_metadata_limits: defaults::chain_wide::METADATA_LIMITS, - asset_definition_metadata_limits: defaults::chain_wide::METADATA_LIMITS, - asset_metadata_limits: defaults::chain_wide::METADATA_LIMITS, - trigger_metadata_limits: defaults::chain_wide::METADATA_LIMITS, - ident_length_limits: defaults::chain_wide::IDENT_LENGTH_LIMITS, - executor_runtime: WasmRuntime::default(), - wasm_runtime: WasmRuntime::default(), - } - } -} - -#[allow(missing_docs)] -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct WasmRuntime { - pub fuel_limit: u64, - pub max_memory: Bytes, -} - -impl Default for WasmRuntime { - fn default() -> Self { - Self { - fuel_limit: defaults::chain_wide::WASM_FUEL_LIMIT, - max_memory: defaults::chain_wide::WASM_MAX_MEMORY, - } - } + pub gossip_size: NonZeroU32, } #[derive(Debug, Clone)] diff --git a/config/src/parameters/defaults.rs b/config/src/parameters/defaults.rs index e68bfbf1c73..aa5e96ce6a9 100644 --- a/config/src/parameters/defaults.rs +++ b/config/src/parameters/defaults.rs @@ -8,7 +8,6 @@ use std::{ time::Duration, }; -use iroha_data_model::{prelude::MetadataLimits, transaction::TransactionLimits, LengthLimits}; use nonzero_ext::nonzero; pub mod queue { @@ -29,10 +28,10 @@ pub mod network { use super::*; pub const TRANSACTION_GOSSIP_PERIOD: Duration = Duration::from_secs(1); - pub const TRANSACTION_GOSSIP_MAX_SIZE: NonZeroU32 = nonzero!(500u32); + pub const TRANSACTION_GOSSIP_SIZE: NonZeroU32 = nonzero!(500u32); pub const BLOCK_GOSSIP_PERIOD: Duration = Duration::from_secs(10); - pub const BLOCK_GOSSIP_MAX_SIZE: NonZeroU32 = nonzero!(4u32); + pub const BLOCK_GOSSIP_SIZE: NonZeroU32 = nonzero!(4u32); pub const IDLE_TIMEOUT: Duration = Duration::from_secs(60); } @@ -45,41 +44,6 @@ pub mod snapshot { pub const CREATE_EVERY: Duration = Duration::from_secs(60); } -pub mod chain_wide { - use iroha_config_base::util::Bytes; - - use super::*; - - pub const MAX_TXS: NonZeroU32 = nonzero!(2_u32.pow(9)); - pub const BLOCK_TIME: Duration = Duration::from_secs(2); - pub const COMMIT_TIME: Duration = Duration::from_secs(4); - pub const WASM_FUEL_LIMIT: u64 = 55_000_000; - pub const WASM_MAX_MEMORY: Bytes = Bytes(500 * 2_u32.pow(20)); - - /// Default estimation of consensus duration. - pub const CONSENSUS_ESTIMATION: Duration = - match BLOCK_TIME.checked_add(match COMMIT_TIME.checked_div(2) { - Some(x) => x, - None => unreachable!(), - }) { - Some(x) => x, - None => unreachable!(), - }; - - /// Default limits for metadata - pub const METADATA_LIMITS: MetadataLimits = MetadataLimits::new(2_u32.pow(20), 2_u32.pow(12)); - /// Default limits for ident length - pub const IDENT_LENGTH_LIMITS: LengthLimits = LengthLimits::new(1, 2_u32.pow(7)); - /// Default maximum number of instructions and expressions per transaction - pub const MAX_INSTRUCTION_NUMBER: u64 = 2_u64.pow(12); - /// Default maximum number of instructions and expressions per transaction - pub const MAX_WASM_SIZE_BYTES: u64 = 4 * 2_u64.pow(20); - - /// Default transaction limits - pub const TRANSACTION_LIMITS: TransactionLimits = - TransactionLimits::new(MAX_INSTRUCTION_NUMBER, MAX_WASM_SIZE_BYTES); -} - pub mod torii { use std::time::Duration; diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs index a619626e896..c3fd636ec8f 100644 --- a/config/src/parameters/user.rs +++ b/config/src/parameters/user.rs @@ -25,10 +25,7 @@ use iroha_config_base::{ ReadConfig, WithOrigin, }; use iroha_crypto::{PrivateKey, PublicKey}; -use iroha_data_model::{ - metadata::Limits as MetadataLimits, peer::PeerId, transaction::TransactionLimits, ChainId, - LengthLimits, Level, -}; +use iroha_data_model::{peer::PeerId, ChainId, Level}; use iroha_primitives::{addr::SocketAddr, unique_vec::UniqueVec}; use serde::Deserialize; use url::Url; @@ -81,8 +78,6 @@ pub struct Root { dev_telemetry: DevTelemetry, #[config(nested)] torii: Torii, - #[config(nested)] - chain_wide: ChainWide, } #[derive(thiserror::Error, Debug, Copy, Clone)] @@ -119,7 +114,6 @@ impl Root { let dev_telemetry = self.dev_telemetry; let (torii, live_query_store) = self.torii.parse(); let telemetry = self.telemetry.map(actual::Telemetry::from); - let chain_wide = self.chain_wide.parse(); let peer_id = key_pair.as_ref().map(|key_pair| { PeerId::new( @@ -156,7 +150,6 @@ impl Root { snapshot, telemetry, dev_telemetry, - chain_wide, }) } } @@ -272,12 +265,12 @@ pub struct Network { /// Peer-to-peer address #[config(env = "P2P_ADDRESS")] pub address: WithOrigin, - #[config(default = "defaults::network::BLOCK_GOSSIP_MAX_SIZE")] - pub block_gossip_max_size: NonZeroU32, + #[config(default = "defaults::network::BLOCK_GOSSIP_SIZE")] + pub block_gossip_size: NonZeroU32, #[config(default = "defaults::network::BLOCK_GOSSIP_PERIOD.into()")] pub block_gossip_period_ms: DurationMs, - #[config(default = "defaults::network::TRANSACTION_GOSSIP_MAX_SIZE")] - pub transaction_gossip_max_size: NonZeroU32, + #[config(default = "defaults::network::TRANSACTION_GOSSIP_SIZE")] + pub transaction_gossip_size: NonZeroU32, #[config(default = "defaults::network::TRANSACTION_GOSSIP_PERIOD.into()")] pub transaction_gossip_period_ms: DurationMs, /// Duration of time after which connection with peer is terminated if peer is idle @@ -295,9 +288,9 @@ impl Network { ) { let Self { address, - block_gossip_max_size, + block_gossip_size, block_gossip_period_ms: block_gossip_period, - transaction_gossip_max_size, + transaction_gossip_size, transaction_gossip_period_ms: transaction_gossip_period, idle_timeout_ms: idle_timeout, } = self; @@ -309,11 +302,11 @@ impl Network { }, actual::BlockSync { gossip_period: block_gossip_period.get(), - gossip_max_size: block_gossip_max_size, + gossip_size: block_gossip_size, }, actual::TransactionGossiper { gossip_period: transaction_gossip_period.get(), - gossip_max_size: transaction_gossip_max_size, + gossip_size: transaction_gossip_size, }, ) } @@ -433,81 +426,6 @@ pub struct Snapshot { pub store_dir: WithOrigin, } -// TODO: make serde -#[derive(Debug, Copy, Clone, ReadConfig)] -pub struct ChainWide { - #[config(default = "defaults::chain_wide::MAX_TXS")] - pub max_transactions_in_block: NonZeroU32, - #[config(default = "defaults::chain_wide::BLOCK_TIME.into()")] - pub block_time_ms: DurationMs, - #[config(default = "defaults::chain_wide::COMMIT_TIME.into()")] - pub commit_time_ms: DurationMs, - #[config(default = "defaults::chain_wide::TRANSACTION_LIMITS")] - pub transaction_limits: TransactionLimits, - #[config(default = "defaults::chain_wide::METADATA_LIMITS")] - pub domain_metadata_limits: MetadataLimits, - #[config(default = "defaults::chain_wide::METADATA_LIMITS")] - pub asset_definition_metadata_limits: MetadataLimits, - #[config(default = "defaults::chain_wide::METADATA_LIMITS")] - pub account_metadata_limits: MetadataLimits, - #[config(default = "defaults::chain_wide::METADATA_LIMITS")] - pub asset_metadata_limits: MetadataLimits, - #[config(default = "defaults::chain_wide::METADATA_LIMITS")] - pub trigger_metadata_limits: MetadataLimits, - #[config(default = "defaults::chain_wide::IDENT_LENGTH_LIMITS")] - pub ident_length_limits: LengthLimits, - #[config(default = "defaults::chain_wide::WASM_FUEL_LIMIT")] - pub executor_fuel_limit: u64, - #[config(default = "defaults::chain_wide::WASM_MAX_MEMORY")] - pub executor_max_memory: Bytes, - #[config(default = "defaults::chain_wide::WASM_FUEL_LIMIT")] - pub wasm_fuel_limit: u64, - #[config(default = "defaults::chain_wide::WASM_MAX_MEMORY")] - pub wasm_max_memory: Bytes, -} - -impl ChainWide { - fn parse(self) -> actual::ChainWide { - let Self { - max_transactions_in_block, - block_time_ms: DurationMs(block_time), - commit_time_ms: DurationMs(commit_time), - transaction_limits, - asset_metadata_limits, - trigger_metadata_limits, - asset_definition_metadata_limits, - account_metadata_limits, - domain_metadata_limits, - ident_length_limits, - executor_fuel_limit, - executor_max_memory, - wasm_fuel_limit, - wasm_max_memory, - } = self; - - actual::ChainWide { - max_transactions_in_block, - block_time, - commit_time, - transaction_limits, - asset_metadata_limits, - trigger_metadata_limits, - asset_definition_metadata_limits, - account_metadata_limits, - domain_metadata_limits, - ident_length_limits, - executor_runtime: actual::WasmRuntime { - fuel_limit: executor_fuel_limit, - max_memory: executor_max_memory, - }, - wasm_runtime: actual::WasmRuntime { - fuel_limit: wasm_fuel_limit, - max_memory: wasm_max_memory, - }, - } - } -} - #[derive(Debug, ReadConfig)] pub struct Torii { #[config(env = "API_ADDRESS")] diff --git a/config/tests/fixtures.rs b/config/tests/fixtures.rs index ae29cb5ba15..411b1460fd8 100644 --- a/config/tests/fixtures.rs +++ b/config/tests/fixtures.rs @@ -162,11 +162,11 @@ fn minimal_config_snapshot() { }, block_sync: BlockSync { gossip_period: 10s, - gossip_max_size: 4, + gossip_size: 4, }, transaction_gossiper: TransactionGossiper { gossip_period: 1s, - gossip_max_size: 500, + gossip_size: 500, }, live_query_store: LiveQueryStore { idle_time: 30s, @@ -197,51 +197,6 @@ fn minimal_config_snapshot() { dev_telemetry: DevTelemetry { out_file: 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, - }, - domain_metadata_limits: Limits { - capacity: 1048576, - max_entry_len: 4096, - }, - asset_definition_metadata_limits: Limits { - capacity: 1048576, - max_entry_len: 4096, - }, - account_metadata_limits: Limits { - capacity: 1048576, - max_entry_len: 4096, - }, - asset_metadata_limits: Limits { - capacity: 1048576, - max_entry_len: 4096, - }, - trigger_metadata_limits: Limits { - capacity: 1048576, - max_entry_len: 4096, - }, - ident_length_limits: LengthLimits { - min: 1, - max: 128, - }, - executor_runtime: WasmRuntime { - fuel_limit: 55000000, - max_memory: Bytes( - 524288000, - ), - }, - wasm_runtime: WasmRuntime { - fuel_limit: 55000000, - max_memory: Bytes( - 524288000, - ), - }, - }, }"#]].assert_eq(&format!("{config:#?}")); } diff --git a/config/tests/fixtures/full.toml b/config/tests/fixtures/full.toml index 02404b6cf3d..22aa92459af 100644 --- a/config/tests/fixtures/full.toml +++ b/config/tests/fixtures/full.toml @@ -11,9 +11,9 @@ signed_file = "genesis.signed.scale" [network] address = "localhost:3840" block_gossip_period_ms = 10_000 -block_gossip_max_size = 4 +block_gossip_size = 4 transaction_gossip_period_ms = 1_000 -transaction_gossip_max_size = 500 +transaction_gossip_size = 500 idle_timeout_ms = 10_000 [torii] @@ -40,8 +40,8 @@ level = "TRACE" format = "compact" [queue] -capacity = 65536 -capacity_per_user = 65536 +capacity = 65_536 +capacity_per_user = 65_536 transaction_time_to_live_ms = 100 future_threshold_ms = 50 @@ -58,16 +58,3 @@ max_retry_delay_exponent = 4 [dev_telemetry] out_file = "./dev_telemetry.json" - -[chain_wide] -max_transactions_in_block = 512 -block_time_ms = 2_000 -commit_time_ms = 4_000 -transaction_limits = { max_instruction_number = 4096, max_wasm_size_bytes = 4194304 } -asset_metadata_limits = { capacity = 1048576, max_entry_len = 4096 } -asset_definition_metadata_limits = { capacity = 1048576, max_entry_len = 4096 } -account_metadata_limits = { capacity = 1048576, max_entry_len = 4096 } -domain_metadata_limits = { capacity = 1048576, max_entry_len = 4096 } -ident_length_limits = { min = 1, max = 128 } -wasm_fuel_limit = 55000000 -wasm_max_memory = 524288000 diff --git a/configs/peer.template.toml b/configs/peer.template.toml index 2c32102420d..0dcfb679abb 100644 --- a/configs/peer.template.toml +++ b/configs/peer.template.toml @@ -20,9 +20,9 @@ [network] # address = # block_gossip_period_ms = 10_000 -# block_gossip_max_size = 4 +# block_gossip_size = 4 # transaction_gossip_period_ms = 1_000 -# transaction_gossip_max_size = 500 +# transaction_gossip_size = 500 # idle_timeout_ms = 60_000 [torii] diff --git a/configs/swarm/executor.wasm b/configs/swarm/executor.wasm index d92c671c9ab6fe1ef4c6d7d63cce9360df4862ab..ee809e63ee9172545a8b464c90d1ad735f72a4c5 100644 GIT binary patch delta 198729 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
    2b6;$z&3*{zylZ? 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�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 diff --git a/configs/swarm/genesis.json b/configs/swarm/genesis.json index b3f7c15ff69..df1170d30ff 100644 --- a/configs/swarm/genesis.json +++ b/configs/swarm/genesis.json @@ -117,48 +117,6 @@ } } }, - { - "NewParameter": "?MaxTransactionsInBlock=512" - }, - { - "NewParameter": "?BlockTime=2000" - }, - { - "NewParameter": "?CommitTimeLimit=4000" - }, - { - "NewParameter": "?TransactionLimits=4096,4194304_TL" - }, - { - "NewParameter": "?WSVDomainMetadataLimits=1048576,4096_ML" - }, - { - "NewParameter": "?WSVAssetDefinitionMetadataLimits=1048576,4096_ML" - }, - { - "NewParameter": "?WSVAccountMetadataLimits=1048576,4096_ML" - }, - { - "NewParameter": "?WSVAssetMetadataLimits=1048576,4096_ML" - }, - { - "NewParameter": "?WSVTriggerMetadataLimits=1048576,4096_ML" - }, - { - "NewParameter": "?WSVIdentLengthLimits=1,128_LL" - }, - { - "NewParameter": "?ExecutorFuelLimit=55000000" - }, - { - "NewParameter": "?ExecutorMaxMemory=524288000" - }, - { - "NewParameter": "?WASMFuelLimit=55000000" - }, - { - "NewParameter": "?WASMMaxMemory=524288000" - }, { "Register": { "Role": { diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index ee3c08b88c8..6ec0bcf3055 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -1,4 +1,4 @@ -use std::str::FromStr as _; +use std::{num::NonZeroU64, str::FromStr as _}; use iroha_core::{ block::{BlockBuilder, CommittedBlock}, @@ -13,8 +13,8 @@ use iroha_data_model::{ asset::{AssetDefinition, AssetDefinitionId}, domain::Domain, isi::InstructionBox, + parameter::TransactionParameters, prelude::*, - transaction::TransactionLimits, ChainId, }; use iroha_primitives::{json::JsonString, unique_vec::UniqueVec}; @@ -34,7 +34,7 @@ pub fn create_block( let transaction = TransactionBuilder::new(chain_id.clone(), account_id) .with_instructions(instructions) .sign(account_private_key); - let limits = state.transaction_executor().transaction_limits; + let limits = state.transaction_executor().limits; let block = BlockBuilder::new( vec![AcceptedTransaction::accept(transaction, &chain_id, limits).unwrap()], @@ -197,9 +197,10 @@ pub fn build_state(rt: &tokio::runtime::Handle, account_id: &AccountId) -> State { let mut state_block = state.block(); - state_block.config.transaction_limits = TransactionLimits::new(u64::MAX, u64::MAX); - state_block.config.executor_runtime.fuel_limit = u64::MAX; - state_block.config.executor_runtime.max_memory = u32::MAX.into(); + state_block.world.parameters.transaction = + TransactionParameters::new(NonZeroU64::MAX, NonZeroU64::MAX); + state_block.world.parameters.executor.fuel = NonZeroU64::MAX; + state_block.world.parameters.executor.memory = NonZeroU64::MAX; let mut state_transaction = state_block.transaction(); let path_to_executor = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/core/benches/kura.rs b/core/benches/kura.rs index bc3b55a49e6..ad00f123533 100644 --- a/core/benches/kura.rs +++ b/core/benches/kura.rs @@ -14,7 +14,8 @@ use iroha_core::{ sumeragi::network_topology::Topology, }; use iroha_crypto::KeyPair; -use iroha_data_model::{prelude::*, transaction::TransactionLimits}; +use iroha_data_model::{parameter::TransactionParameters, prelude::*}; +use nonzero_ext::nonzero; use test_samples::gen_account_in; use tokio::{fs, runtime::Runtime}; @@ -29,11 +30,11 @@ async fn measure_block_size_for_n_executors(n_executors: u32) { let tx = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions([transfer]) .sign(alice_keypair.private_key()); - let transaction_limits = TransactionLimits { - max_instruction_number: 4096, - max_wasm_size_bytes: 0, + let txn_limits = TransactionParameters { + max_instructions: nonzero!(4096_u64), + smart_contract_size: nonzero!(1_u64), }; - let tx = AcceptedTransaction::accept(tx, &chain_id, transaction_limits) + let tx = AcceptedTransaction::accept(tx, &chain_id, txn_limits) .expect("Failed to accept Transaction."); let dir = tempfile::tempdir().expect("Could not create tempfile."); let cfg = Config { diff --git a/core/benches/validation.rs b/core/benches/validation.rs index 4814d0dd2ca..91feaa7a9da 100644 --- a/core/benches/validation.rs +++ b/core/benches/validation.rs @@ -11,12 +11,11 @@ use iroha_core::{ tx::TransactionExecutor, }; use iroha_data_model::{ - account::AccountId, - isi::InstructionBox, - prelude::*, - transaction::{TransactionBuilder, TransactionLimits}, + account::AccountId, isi::InstructionBox, parameter::TransactionParameters, prelude::*, + transaction::TransactionBuilder, }; use iroha_primitives::unique_vec::UniqueVec; +use nonzero_ext::nonzero; use once_cell::sync::Lazy; use test_samples::gen_account_in; @@ -25,10 +24,8 @@ static STARTER_KEYPAIR: Lazy = Lazy::new(KeyPair::random); static STARTER_ID: Lazy = Lazy::new(|| AccountId::new(STARTER_DOMAIN.clone(), STARTER_KEYPAIR.public_key().clone())); -const TRANSACTION_LIMITS: TransactionLimits = TransactionLimits { - max_instruction_number: 4096, - max_wasm_size_bytes: 0, -}; +const TRANSACTION_LIMITS: TransactionParameters = + TransactionParameters::new(nonzero!(4096_u64), nonzero!(1_u64)); fn build_test_transaction(chain_id: ChainId) -> TransactionBuilder { let domain_id: DomainId = "domain".parse().unwrap(); diff --git a/core/src/block.rs b/core/src/block.rs index aa96a785fd7..8a4f4e9985d 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -107,7 +107,10 @@ pub enum InvalidGenesisError { pub struct BlockBuilder(B); mod pending { - use std::time::{Duration, SystemTime}; + use std::{ + num::NonZeroUsize, + time::{Duration, SystemTime}, + }; use iroha_data_model::transaction::CommittedTransaction; @@ -156,11 +159,14 @@ mod pending { consensus_estimation: Duration, ) -> BlockHeader { BlockHeader { - height: prev_height - .checked_add(1) - .expect("INTERNAL BUG: Blockchain height exceeds usize::MAX") - .try_into() - .expect("INTERNAL BUG: Number of blocks exceeds u64::MAX"), + height: NonZeroUsize::new( + prev_height + .checked_add(1) + .expect("INTERNAL BUG: Blockchain height exceeds usize::MAX"), + ) + .expect("INTERNAL BUG: block height must not be 0") + .try_into() + .expect("INTERNAL BUG: Number of blocks exceeds u64::MAX"), prev_block_hash, transactions_hash: transactions .iter() @@ -228,7 +234,7 @@ mod pending { state.latest_block_hash(), view_change_index, &transactions, - state.config.consensus_estimation(), + state.world.parameters().sumeragi.consensus_estimation(), ), transactions, commit_topology: self.0.commit_topology.into_iter().collect(), @@ -414,10 +420,14 @@ mod valid { genesis_account: &AccountId, state_block: &mut StateBlock<'_>, ) -> WithEvents> { - let expected_block_height = state_block.height() + 1; + let expected_block_height = state_block + .height() + .checked_add(1) + .expect("INTERNAL BUG: Block height exceeds usize::MAX"); let actual_height = block .header() .height + .get() .try_into() .expect("INTERNAL BUG: Block height exceeds usize::MAX"); @@ -516,7 +526,7 @@ mod valid { AcceptedTransaction::accept( value, expected_chain_id, - transaction_executor.transaction_limits, + transaction_executor.limits, ) }?; @@ -638,9 +648,11 @@ mod valid { leader_private_key: &PrivateKey, f: impl FnOnce(&mut BlockPayload), ) -> Self { + use nonzero_ext::nonzero; + let mut payload = BlockPayload { header: BlockHeader { - height: 2, + height: nonzero!(2_u64), prev_block_hash: None, transactions_hash: HashOf::from_untyped_unchecked(Hash::prehashed( [1; Hash::LENGTH], @@ -997,7 +1009,7 @@ mod tests { Register::asset_definition(AssetDefinition::numeric(asset_definition_id)); // Making two transactions that have the same instruction - let transaction_limits = state_block.transaction_executor().transaction_limits; + let transaction_limits = state_block.transaction_executor().limits; let tx = TransactionBuilder::new(chain_id.clone(), alice_id) .with_instructions([create_asset_definition]) .sign(alice_keypair.private_key()); @@ -1053,7 +1065,7 @@ mod tests { Register::asset_definition(AssetDefinition::numeric(asset_definition_id.clone())); // Making two transactions that have the same instruction - let transaction_limits = state_block.transaction_executor().transaction_limits; + let transaction_limits = state_block.transaction_executor().limits; let tx = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions([create_asset_definition]) .sign(alice_keypair.private_key()); @@ -1120,7 +1132,7 @@ mod tests { let query_handle = LiveQueryStore::test().start(); let state = State::new(world, kura, query_handle); let mut state_block = state.block(); - let transaction_limits = state_block.transaction_executor().transaction_limits; + let transaction_limits = state_block.transaction_executor().limits; let domain_id = DomainId::from_str("domain").expect("Valid"); let create_domain = Register::domain(Domain::new(domain_id)); diff --git a/core/src/block_sync.rs b/core/src/block_sync.rs index 85dd12ff289..99553729f92 100644 --- a/core/src/block_sync.rs +++ b/core/src/block_sync.rs @@ -41,7 +41,7 @@ pub struct BlockSynchronizer { kura: Arc, peer_id: PeerId, gossip_period: Duration, - gossip_max_size: NonZeroU32, + gossip_size: NonZeroU32, network: IrohaNetwork, state: Arc, } @@ -118,7 +118,7 @@ impl BlockSynchronizer { sumeragi, kura, gossip_period: config.gossip_period, - gossip_max_size: config.gossip_max_size, + gossip_size: config.gossip_size, network, state, } @@ -219,7 +219,7 @@ pub mod message { }; let blocks = (start_height.get()..) - .take(block_sync.gossip_max_size.get() as usize + 1) + .take(block_sync.gossip_size.get() as usize + 1) .map_while(|height| { NonZeroUsize::new(height) .and_then(|height| block_sync.kura.get_block_by_height(height)) diff --git a/core/src/executor.rs b/core/src/executor.rs index 28c2ff9f7a0..ab0bd0b2b12 100644 --- a/core/src/executor.rs +++ b/core/src/executor.rs @@ -18,6 +18,7 @@ use serde::{ use crate::{ smartcontracts::{wasm, Execute as _}, state::{deserialize::WasmSeed, StateReadOnly, StateTransaction}, + WorldReadOnly as _, }; impl From for ValidationFail { @@ -151,7 +152,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::::new() .with_engine(state_transaction.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_config(state_transaction.config.executor_runtime) + .with_config(state_transaction.world.parameters().executor) .build()?; runtime.execute_executor_validate_transaction( @@ -187,7 +188,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::::new() .with_engine(state_transaction.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_config(state_transaction.config.executor_runtime) + .with_config(state_transaction.world.parameters().executor) .build()?; runtime.execute_executor_validate_instruction( @@ -221,7 +222,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::>::new() .with_engine(state_ro.engine().clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_config(state_ro.config().executor_runtime) + .with_config(state_ro.world().parameters().executor) .build()?; runtime.execute_executor_validate_query( @@ -256,7 +257,7 @@ impl Executor { let runtime = wasm::RuntimeBuilder::::new() .with_engine(state_transaction.engine.clone()) // Cloning engine is cheap, see [`wasmtime::Engine`] docs - .with_config(state_transaction.config.executor_runtime) + .with_config(state_transaction.world().parameters().executor) .build()?; runtime diff --git a/core/src/gossiper.rs b/core/src/gossiper.rs index 4f5018aa9f6..4a08606108e 100644 --- a/core/src/gossiper.rs +++ b/core/src/gossiper.rs @@ -8,7 +8,10 @@ use iroha_p2p::Broadcast; use parity_scale_codec::{Decode, Encode}; use tokio::sync::mpsc; -use crate::{queue::Queue, state::State, tx::AcceptedTransaction, IrohaNetwork, NetworkMessage}; +use crate::{ + queue::Queue, state::State, tx::AcceptedTransaction, IrohaNetwork, NetworkMessage, + StateReadOnly, WorldReadOnly, +}; /// [`Gossiper`] actor handle. #[derive(Clone)] @@ -26,21 +29,18 @@ impl TransactionGossiperHandle { } } -/// Actor to gossip transactions and receive transaction gossips +/// Actor which gossips transactions and receives transaction gossips pub struct TransactionGossiper { /// Unique id of the blockchain. Used for simple replay attack protection. 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_max_size: NonZeroU32, - /// The time between gossiping. More frequent gossiping shortens + /// The time between gossip messages. More frequent gossiping shortens /// the time to sync, but can overload the network. gossip_period: Duration, - /// Address of queue - queue: Arc, - /// [`iroha_p2p::Network`] actor handle + /// Maximum size of a batch that is being gossiped. Smaller size leads + /// to longer time to synchronise, useful if you have high packet loss. + gossip_size: NonZeroU32, network: IrohaNetwork, - /// [`WorldState`] + queue: Arc, state: Arc, } @@ -57,7 +57,7 @@ impl TransactionGossiper { chain_id: ChainId, Config { gossip_period, - gossip_max_size, + gossip_size, }: Config, network: IrohaNetwork, queue: Arc, @@ -65,10 +65,10 @@ impl TransactionGossiper { ) -> Self { Self { chain_id, - gossip_max_size, gossip_period, - queue, + gossip_size, network, + queue, state, } } @@ -93,7 +93,7 @@ impl TransactionGossiper { fn gossip_transactions(&self) { let txs = self .queue - .n_random_transactions(self.gossip_max_size.get(), &self.state.view()); + .n_random_transactions(self.gossip_size.get(), &self.state.view()); if txs.is_empty() { return; @@ -110,7 +110,7 @@ impl TransactionGossiper { let state_view = self.state.view(); for tx in txs { - let transaction_limits = state_view.config.transaction_limits; + let transaction_limits = state_view.world().parameters().transaction; match AcceptedTransaction::accept(tx, &self.chain_id, transaction_limits) { Ok(tx) => match self.queue.push(tx, &state_view) { diff --git a/core/src/lib.rs b/core/src/lib.rs index f1104d47163..0d25bf98ca7 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -18,7 +18,6 @@ pub mod tx; use core::time::Duration; use gossiper::TransactionGossip; -use indexmap::IndexSet; use iroha_data_model::{events::EventBox, prelude::*}; use iroha_primitives::unique_vec::UniqueVec; use parity_scale_codec::{Decode, Encode}; @@ -39,9 +38,6 @@ pub type IrohaNetwork = iroha_p2p::NetworkHandle; /// Ids of peers. pub type PeersIds = UniqueVec; -/// Parameters set. -pub type Parameters = IndexSet; - /// Type of `Sender` which should be used for channels of `Event` messages. pub type EventsSender = broadcast::Sender; diff --git a/core/src/query/store.rs b/core/src/query/store.rs index 1548567f3b3..490b83d2954 100644 --- a/core/src/query/store.rs +++ b/core/src/query/store.rs @@ -8,7 +8,11 @@ use std::{ use indexmap::IndexMap; use iroha_config::parameters::actual::LiveQueryStore as Config; use iroha_data_model::{ - query::{cursor::ForwardCursor, error::QueryExecutionFail, QueryId, QueryOutputBox}, + query::{ + cursor::{ForwardCursor, QueryId}, + error::QueryExecutionFail, + QueryOutputBox, + }, BatchedResponse, BatchedResponseV1, ValidationFail, }; use iroha_logger::trace; diff --git a/core/src/queue.rs b/core/src/queue.rs index ebea3f56634..d3ac864685b 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -284,9 +284,9 @@ impl Queue { fn collect_transactions_for_block( &self, state_view: &StateView, - max_txs_in_block: usize, + max_txs_in_block: NonZeroUsize, ) -> Vec { - let mut transactions = Vec::with_capacity(max_txs_in_block); + let mut transactions = Vec::with_capacity(max_txs_in_block.get()); self.get_transactions_for_block(state_view, max_txs_in_block, &mut transactions); transactions } @@ -297,10 +297,10 @@ impl Queue { pub fn get_transactions_for_block( &self, state_view: &StateView, - max_txs_in_block: usize, + max_txs_in_block: NonZeroUsize, transactions: &mut Vec, ) { - if transactions.len() >= max_txs_in_block { + if transactions.len() >= max_txs_in_block.get() { return; } @@ -315,7 +315,7 @@ impl Queue { transactions.iter().map(|tx| tx.as_ref().hash()).collect(); let txs = txs_from_queue .filter(|tx| !transactions_hashes.contains(&tx.as_ref().hash())) - .take(max_txs_in_block - transactions.len()); + .take(max_txs_in_block.get() - transactions.len()); transactions.extend(txs); seen_queue @@ -377,7 +377,7 @@ impl Queue { pub mod tests { use std::{str::FromStr, sync::Arc, thread, time::Duration}; - use iroha_data_model::{prelude::*, transaction::TransactionLimits}; + use iroha_data_model::{parameter::TransactionParameters, prelude::*}; use nonzero_ext::nonzero; use rand::Rng as _; use test_samples::gen_account_in; @@ -425,9 +425,9 @@ pub mod tests { TransactionBuilder::new_with_time_source(chain_id.clone(), account_id, time_source) .with_instructions(instructions) .sign(key_pair.private_key()); - let limits = TransactionLimits { - max_instruction_number: 4096, - max_wasm_size_bytes: 0, + let limits = TransactionParameters { + max_instructions: nonzero!(4096_u64), + smart_contract_size: nonzero!(1024_u64), }; AcceptedTransaction::accept(tx, &chain_id, limits).expect("Failed to accept Transaction.") } @@ -502,7 +502,7 @@ pub mod tests { #[test] async fn get_available_txs() { - let max_txs_in_block = 2; + let max_txs_in_block = nonzero!(2_usize); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); let state = Arc::new(State::new(world_with_test_domains(), kura, query_handle)); @@ -525,7 +525,7 @@ pub mod tests { } let available = queue.collect_transactions_for_block(&state_view, max_txs_in_block); - assert_eq!(available.len(), max_txs_in_block); + assert_eq!(available.len(), max_txs_in_block.get()); } #[test] @@ -536,7 +536,9 @@ pub mod tests { let (_time_handle, time_source) = TimeSource::new_mock(Duration::default()); let tx = accepted_tx_by_someone(&time_source); let mut state_block = state.block(); - state_block.transactions.insert(tx.as_ref().hash(), 1); + state_block + .transactions + .insert(tx.as_ref().hash(), nonzero!(1_usize)); state_block.commit(); let state_view = state.view(); let queue = Queue::test(config_factory(), &time_source); @@ -552,7 +554,7 @@ pub mod tests { #[test] async fn get_tx_drop_if_in_blockchain() { - let max_txs_in_block = 2; + let max_txs_in_block = nonzero!(2_usize); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); let state = State::new(world_with_test_domains(), kura, query_handle); @@ -561,7 +563,9 @@ pub mod tests { let queue = Queue::test(config_factory(), &time_source); queue.push(tx.clone(), &state.view()).unwrap(); let mut state_block = state.block(); - state_block.transactions.insert(tx.as_ref().hash(), 1); + state_block + .transactions + .insert(tx.as_ref().hash(), nonzero!(1_usize)); state_block.commit(); assert_eq!( queue @@ -574,7 +578,7 @@ pub mod tests { #[test] async fn get_available_txs_with_timeout() { - let max_txs_in_block = 6; + let max_txs_in_block = nonzero!(6_usize); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); let state = Arc::new(State::new(world_with_test_domains(), kura, query_handle)); @@ -589,7 +593,7 @@ pub mod tests { }, &time_source, ); - for _ in 0..(max_txs_in_block - 1) { + for _ in 0..(max_txs_in_block.get() - 1) { queue .push(accepted_tx_by_someone(&time_source), &state_view) .expect("Failed to push tx into queue"); @@ -623,7 +627,7 @@ pub mod tests { // Others should stay in the queue until that moment. #[test] async fn transactions_available_after_pop() { - let max_txs_in_block = 2; + let max_txs_in_block = nonzero!(2_usize); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); let state = Arc::new(State::new(world_with_test_domains(), kura, query_handle)); @@ -656,7 +660,7 @@ pub mod tests { let chain_id = ChainId::from("00000000-0000-0000-0000-000000000000"); - let max_txs_in_block = 2; + let max_txs_in_block = nonzero!(2_usize); let (alice_id, alice_keypair) = gen_account_in("wonderland"); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); @@ -674,9 +678,9 @@ pub mod tests { .with_instructions(instructions); tx.set_ttl(Duration::from_millis(TTL_MS)); let tx = tx.sign(alice_keypair.private_key()); - let limits = TransactionLimits { - max_instruction_number: 4096, - max_wasm_size_bytes: 0, + let limits = TransactionParameters { + max_instructions: nonzero!(4096_u64), + smart_contract_size: nonzero!(1024_u64), }; let tx_hash = tx.hash(); let tx = AcceptedTransaction::accept(tx, &chain_id, limits) @@ -715,7 +719,7 @@ pub mod tests { #[test] async fn concurrent_stress_test() { - let max_txs_in_block = 10; + let max_txs_in_block = nonzero!(10_usize); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); let state = Arc::new(State::new(world_with_test_domains(), kura, query_handle)); @@ -763,7 +767,9 @@ pub mod tests { for tx in queue.collect_transactions_for_block(&state.view(), max_txs_in_block) { let mut state_block = state.block(); - state_block.transactions.insert(tx.as_ref().hash(), 1); + state_block + .transactions + .insert(tx.as_ref().hash(), nonzero!(1_usize)); state_block.commit(); } // Simulate random small delays @@ -881,18 +887,18 @@ pub mod tests { ) .expect("Failed to push tx into queue"); - let transactions = queue.collect_transactions_for_block(&state.view(), 10); + let transactions = queue.collect_transactions_for_block(&state.view(), nonzero!(10_usize)); assert_eq!(transactions.len(), 2); let mut state_block = state.block(); for transaction in transactions { // Put transaction hashes into state as if they were in the blockchain state_block .transactions - .insert(transaction.as_ref().hash(), 1); + .insert(transaction.as_ref().hash(), nonzero!(1_usize)); } state_block.commit(); // Cleanup transactions - let transactions = queue.collect_transactions_for_block(&state.view(), 10); + let transactions = queue.collect_transactions_for_block(&state.view(), nonzero!(10_usize)); assert!(transactions.is_empty()); // After cleanup Alice and Bob pushes should work fine diff --git a/core/src/smartcontracts/isi/account.rs b/core/src/smartcontracts/isi/account.rs index 271e0333dd1..336b0e2c60e 100644 --- a/core/src/smartcontracts/isi/account.rs +++ b/core/src/smartcontracts/isi/account.rs @@ -28,7 +28,7 @@ pub mod isi { asset::{AssetValue, AssetValueType}, isi::{ error::{MintabilityError, RepetitionError}, - InstructionType, + InstructionId, }, query::error::QueryExecutionFail, }; @@ -78,7 +78,7 @@ pub mod isi { _ => Err(err.into()), }, Ok(_) => Err(RepetitionError { - instruction_type: InstructionType::Register, + instruction_type: InstructionId::Register, id: IdBox::AssetId(asset_id.clone()), } .into()), @@ -174,21 +174,14 @@ pub mod isi { ) -> Result<(), Error> { let account_id = self.object; - let account_metadata_limits = state_transaction.config.account_metadata_limits; - state_transaction .world .account_mut(&account_id) .map_err(Error::from) - .and_then(|account| { + .map(|account| { account .metadata - .insert_with_limits( - self.key.clone(), - self.value.clone(), - account_metadata_limits, - ) - .map_err(Error::from) + .insert(self.key.clone(), self.value.clone()) })?; state_transaction @@ -253,7 +246,7 @@ pub mod isi { .account_contains_inherent_permission(&account_id, &permission) { return Err(RepetitionError { - instruction_type: InstructionType::Grant, + instruction_type: InstructionId::Grant, id: permission.id.into(), } .into()); @@ -341,7 +334,7 @@ pub mod isi { .is_some() { return Err(RepetitionError { - instruction_type: InstructionType::Grant, + instruction_type: InstructionId::Grant, id: IdBox::RoleId(role_id), } .into()); diff --git a/core/src/smartcontracts/isi/asset.rs b/core/src/smartcontracts/isi/asset.rs index a8ae9462965..0211f22f5f5 100644 --- a/core/src/smartcontracts/isi/asset.rs +++ b/core/src/smartcontracts/isi/asset.rs @@ -62,21 +62,16 @@ pub mod isi { .increase_asset_total_amount(&asset_id.definition, Numeric::ONE)?; } - let asset_metadata_limits = state_transaction.config.asset_metadata_limits; let asset = state_transaction .world - .asset_or_insert(asset_id.clone(), Metadata::new())?; + .asset_or_insert(asset_id.clone(), Metadata::default())?; { let AssetValue::Store(store) = &mut asset.value else { return Err(Error::Conversion("Expected store asset type".to_owned())); }; - store.insert_with_limits( - self.key.clone(), - self.value.clone(), - asset_metadata_limits, - )?; + store.insert(self.key.clone(), self.value.clone()); } state_transaction diff --git a/core/src/smartcontracts/isi/domain.rs b/core/src/smartcontracts/isi/domain.rs index 415bdd1ac81..76c93f35e94 100644 --- a/core/src/smartcontracts/isi/domain.rs +++ b/core/src/smartcontracts/isi/domain.rs @@ -57,7 +57,7 @@ pub mod isi { let _domain = state_transaction.world.domain_mut(&account_id.domain)?; if state_transaction.world.account(&account_id).is_ok() { return Err(RepetitionError { - instruction_type: InstructionType::Register, + instruction_type: InstructionId::Register, id: IdBox::AccountId(account_id), } .into()); @@ -134,11 +134,6 @@ pub mod isi { state_transaction: &mut StateTransaction<'_, '_>, ) -> Result<(), Error> { let asset_definition = self.object.build(authority); - asset_definition - .id() - .name - .validate_len(state_transaction.config.ident_length_limits) - .map_err(Error::from)?; let asset_definition_id = asset_definition.id().clone(); let domain = state_transaction @@ -146,7 +141,7 @@ pub mod isi { .domain_mut(&asset_definition_id.domain)?; if domain.asset_definitions.contains_key(&asset_definition_id) { return Err(RepetitionError { - instruction_type: InstructionType::Register, + instruction_type: InstructionId::Register, id: IdBox::AssetDefinitionId(asset_definition_id), } .into()); @@ -219,7 +214,7 @@ pub mod isi { domain.remove_asset_total_quantity(&asset_definition_id); events.push(DataEvent::from(DomainEvent::AssetDefinition( - AssetDefinitionEvent::Deleted(asset_definition_id), + AssetDefinitionEvent::Delete(asset_definition_id), ))); state_transaction.world.emit_events(events); @@ -237,16 +232,14 @@ pub mod isi { ) -> Result<(), Error> { let asset_definition_id = self.object; - let metadata_limits = state_transaction.config.asset_definition_metadata_limits; state_transaction .world .asset_definition_mut(&asset_definition_id) .map_err(Error::from) - .and_then(|asset_definition| { + .map(|asset_definition| { asset_definition .metadata - .insert_with_limits(self.key.clone(), self.value.clone(), metadata_limits) - .map_err(Error::from) + .insert(self.key.clone(), self.value.clone()) })?; state_transaction @@ -305,12 +298,8 @@ pub mod isi { ) -> Result<(), Error> { let domain_id = self.object; - let limits = state_transaction.config.domain_metadata_limits; - let domain = state_transaction.world.domain_mut(&domain_id)?; - domain - .metadata - .insert_with_limits(self.key.clone(), self.value.clone(), limits)?; + domain.metadata.insert(self.key.clone(), self.value.clone()); state_transaction .world diff --git a/core/src/smartcontracts/isi/mod.rs b/core/src/smartcontracts/isi/mod.rs index deb2c4d0657..156473d0a7d 100644 --- a/core/src/smartcontracts/isi/mod.rs +++ b/core/src/smartcontracts/isi/mod.rs @@ -54,7 +54,6 @@ impl Execute for InstructionBox { Self::Revoke(isi) => isi.execute(authority, state_transaction), Self::ExecuteTrigger(isi) => isi.execute(authority, state_transaction), Self::SetParameter(isi) => isi.execute(authority, state_transaction), - Self::NewParameter(isi) => isi.execute(authority, state_transaction), Self::Upgrade(isi) => isi.execute(authority, state_transaction), Self::Log(isi) => isi.execute(authority, state_transaction), Self::Custom(_) => { @@ -442,7 +441,7 @@ mod tests { let tx = TransactionBuilder::new(chain_id.clone(), SAMPLE_GENESIS_ACCOUNT_ID.clone()) .with_instructions(instructions) .sign(SAMPLE_GENESIS_ACCOUNT_KEYPAIR.private_key()); - let tx_limits = state_block.transaction_executor().transaction_limits; + let tx_limits = state_block.transaction_executor().limits; assert!(matches!( AcceptedTransaction::accept(tx, &chain_id, tx_limits), Err(AcceptTransactionFail::UnexpectedGenesisAccountSignature) diff --git a/core/src/smartcontracts/isi/query.rs b/core/src/smartcontracts/isi/query.rs index fcc0358ed8e..4702533ad8c 100644 --- a/core/src/smartcontracts/isi/query.rs +++ b/core/src/smartcontracts/isi/query.rs @@ -169,6 +169,7 @@ impl_lazy! { iroha_data_model::query::TransactionQueryOutput, iroha_data_model::executor::ExecutorDataModel, iroha_data_model::trigger::Trigger, + iroha_data_model::parameter::Parameters, } /// Query Request statefully validated on the Iroha node side. @@ -256,6 +257,7 @@ impl ValidQuery for QueryBox { FindAssetDefinitionKeyValueByIdAndKey, FindTriggerKeyValueByIdAndKey, FindExecutorDataModel, + FindAllParameters, } FindAllAccounts, @@ -281,7 +283,6 @@ impl ValidQuery for QueryBox { FindAllRoles, FindAllRoleIds, FindRolesByAccountId, - FindAllParameters, } } } @@ -291,8 +292,9 @@ mod tests { use std::str::FromStr as _; use iroha_crypto::{Hash, HashOf, KeyPair}; - use iroha_data_model::{query::error::FindError, transaction::TransactionLimits}; + use iroha_data_model::{parameter::TransactionParameters, query::error::FindError}; use iroha_primitives::json::JsonString; + use nonzero_ext::nonzero; use test_samples::{gen_account_in, ALICE_ID, ALICE_KEYPAIR}; use tokio::test; @@ -330,12 +332,11 @@ mod tests { ) .is_none()); - let mut store = Metadata::new(); + let mut store = Metadata::default(); store - .insert_with_limits( + .insert( Name::from_str("Bytes").expect("Valid"), vec![1_u32, 2_u32, 3_u32], - MetadataLimits::new(10, 100), ) .unwrap(); let asset_id = AssetId::new(asset_definition_id, account.id().clone()); @@ -346,12 +347,8 @@ mod tests { } fn world_with_test_account_with_metadata() -> Result { - let mut metadata = Metadata::new(); - metadata.insert_with_limits( - Name::from_str("Bytes")?, - vec![1_u32, 2_u32, 3_u32], - MetadataLimits::new(10, 100), - )?; + let mut metadata = Metadata::default(); + metadata.insert(Name::from_str("Bytes")?, vec![1_u32, 2_u32, 3_u32]); let mut domain = Domain::new(DomainId::from_str("wonderland")?).build(&ALICE_ID); let account = Account::new(ALICE_ID.clone()) @@ -376,16 +373,16 @@ mod tests { let state = State::new(world_with_test_domains(), kura.clone(), query_handle); { let mut state_block = state.block(); - let limits = TransactionLimits { - max_instruction_number: 1, - max_wasm_size_bytes: 0, + let limits = TransactionParameters { + max_instructions: nonzero!(1000_u64), + smart_contract_size: nonzero!(1024_u64), }; - let huge_limits = TransactionLimits { - max_instruction_number: 1000, - max_wasm_size_bytes: 0, + let huge_limits = TransactionParameters { + max_instructions: nonzero!(1000_u64), + smart_contract_size: nonzero!(1024_u64), }; - state_block.config.transaction_limits = limits; + state_block.world.parameters.transaction = limits; let valid_tx = { let instructions: [InstructionBox; 0] = []; @@ -554,7 +551,7 @@ mod tests { .with_instructions(instructions) .sign(ALICE_KEYPAIR.private_key()); - let tx_limits = state_block.transaction_executor().transaction_limits; + let tx_limits = state_block.transaction_executor().limits; let va_tx = AcceptedTransaction::accept(tx, &chain_id, tx_limits)?; let (peer_public_key, _) = KeyPair::random().into_parts(); @@ -599,12 +596,8 @@ mod tests { async fn domain_metadata() -> Result<()> { let kura = Kura::blank_kura_for_testing(); let state = { - let mut metadata = Metadata::new(); - metadata.insert_with_limits( - Name::from_str("Bytes")?, - vec![1_u32, 2_u32, 3_u32], - MetadataLimits::new(10, 100), - )?; + let mut metadata = Metadata::default(); + metadata.insert(Name::from_str("Bytes")?, vec![1_u32, 2_u32, 3_u32]); let mut domain = Domain::new(DomainId::from_str("wonderland")?) .with_metadata(metadata) .build(&ALICE_ID); diff --git a/core/src/smartcontracts/isi/triggers/mod.rs b/core/src/smartcontracts/isi/triggers/mod.rs index b1af89f0af7..66482f0d22f 100644 --- a/core/src/smartcontracts/isi/triggers/mod.rs +++ b/core/src/smartcontracts/isi/triggers/mod.rs @@ -99,7 +99,7 @@ pub mod isi { if !success { return Err(RepetitionError { - instruction_type: InstructionType::Register, + instruction_type: InstructionId::Register, id: trigger_id.into(), } .into()); @@ -130,7 +130,7 @@ pub mod isi { Ok(()) } else { Err(RepetitionError { - instruction_type: InstructionType::Unregister, + instruction_type: InstructionId::Unregister, id: trigger_id.into(), } .into()) @@ -213,18 +213,15 @@ pub mod isi { ) -> Result<(), Error> { let trigger_id = self.object; - let trigger_metadata_limits = state_transaction.config.account_metadata_limits; state_transaction .world .triggers .inspect_by_id_mut(&trigger_id, |action| { - action.metadata_mut().insert_with_limits( - self.key.clone(), - self.value.clone(), - trigger_metadata_limits, - ) + action + .metadata_mut() + .insert(self.key.clone(), self.value.clone()) }) - .ok_or(FindError::Trigger(trigger_id.clone()))??; + .ok_or(FindError::Trigger(trigger_id.clone()))?; state_transaction .world diff --git a/core/src/smartcontracts/isi/triggers/specialized.rs b/core/src/smartcontracts/isi/triggers/specialized.rs index 5409cf8a070..a3deafa5d83 100644 --- a/core/src/smartcontracts/isi/triggers/specialized.rs +++ b/core/src/smartcontracts/isi/triggers/specialized.rs @@ -45,7 +45,7 @@ impl SpecializedAction { // TODO: At this point the authority is meaningless. authority, filter, - metadata: Metadata::new(), + metadata: Metadata::default(), } } } diff --git a/core/src/smartcontracts/isi/world.rs b/core/src/smartcontracts/isi/world.rs index e955151a390..62130dcecad 100644 --- a/core/src/smartcontracts/isi/world.rs +++ b/core/src/smartcontracts/isi/world.rs @@ -20,11 +20,12 @@ pub mod isi { use eyre::Result; use iroha_data_model::{ isi::error::{InstructionExecutionError, InvalidParameterError, RepetitionError}, + parameter::{CustomParameter, Parameter}, prelude::*, query::error::FindError, Level, }; - use iroha_primitives::unique_vec::PushResult; + use iroha_primitives::{json::JsonString, unique_vec::PushResult}; use super::*; @@ -41,7 +42,7 @@ pub mod isi { if let PushResult::Duplicate(duplicate) = world.trusted_peers_ids.push(peer_id.clone()) { return Err(RepetitionError { - instruction_type: InstructionType::Register, + instruction_type: InstructionId::Register, id: IdBox::PeerId(duplicate), } .into()); @@ -84,11 +85,6 @@ pub mod isi { let domain: Domain = self.object.build(authority); let domain_id = domain.id().clone(); - domain_id - .name - .validate_len(state_transaction.config.ident_length_limits) - .map_err(Error::from)?; - if domain_id == *iroha_genesis::GENESIS_DOMAIN_ID { return Err(InstructionExecutionError::InvariantViolation( "Not allowed to register genesis domain".to_owned(), @@ -98,14 +94,13 @@ pub mod isi { let world = &mut state_transaction.world; if world.domains.get(&domain_id).is_some() { return Err(RepetitionError { - instruction_type: InstructionType::Register, + instruction_type: InstructionId::Register, id: IdBox::DomainId(domain_id), } .into()); } world.domains.insert(domain_id, domain.clone()); - world.emit_events(Some(DomainEvent::Created(domain))); Ok(()) @@ -183,7 +178,7 @@ pub mod isi { if state_transaction.world.roles.get(role.id()).is_some() { return Err(RepetitionError { - instruction_type: InstructionType::Register, + instruction_type: InstructionId::Register, id: IdBox::RoleId(role.id), } .into()); @@ -253,7 +248,7 @@ pub mod isi { if !role.permissions.insert(permission.clone()) { return Err(RepetitionError { - instruction_type: InstructionType::Grant, + instruction_type: InstructionId::Grant, id: permission.id.into(), } .into()); @@ -307,43 +302,70 @@ pub mod isi { _authority: &AccountId, state_transaction: &mut StateTransaction<'_, '_>, ) -> Result<(), Error> { - let parameter = self.parameter; - let parameter_id = parameter.id.clone(); - - if !state_transaction.world.parameters.swap_remove(¶meter) { - return Err(FindError::Parameter(parameter_id).into()); + macro_rules! set_parameter { + ($($container:ident($param:ident.$field:ident) => $single:ident::$variant:ident),* $(,)?) => { + match self.0 { $( + Parameter::$container(iroha_data_model::parameter::$single::$variant(next)) => { + let prev = core::mem::replace( + &mut state_transaction.world.parameters.$param.$field, + next, + ); + + state_transaction.world.emit_events( + Some(ConfigurationEvent::Changed(ParameterChanged { + prev: Parameter::$container(iroha_data_model::parameter::$single::$variant( + prev, + )), + next: Parameter::$container(iroha_data_model::parameter::$single::$variant( + next, + )), + })) + ); + })* + Parameter::Custom(next) => { + let prev = state_transaction + .world + .parameters + .custom + .insert(next.id.clone(), next.clone()) + .unwrap_or_else(|| { + iroha_logger::error!( + "{}: Initial parameter value not set during executor migration", + next.id + ); + + CustomParameter { + id: next.id.clone(), + payload: JsonString::default(), + } + }); + + state_transaction + .world + .emit_events(Some(ConfigurationEvent::Changed(ParameterChanged { + prev: Parameter::Custom(prev), + next: Parameter::Custom(next), + }))); + } + } + }; } - state_transaction.world.parameters.insert(parameter.clone()); - state_transaction - .world - .emit_events(Some(ConfigurationEvent::Changed(parameter_id))); - state_transaction.try_apply_core_parameter(parameter); - Ok(()) - } - } + set_parameter!( + Sumeragi(sumeragi.block_time_ms) => SumeragiParameter::BlockTimeMs, + Sumeragi(sumeragi.commit_time_ms) => SumeragiParameter::CommitTimeMs, - impl Execute for NewParameter { - #[metrics(+"new_parameter")] - fn execute( - self, - _authority: &AccountId, - state_transaction: &mut StateTransaction<'_, '_>, - ) -> Result<(), Error> { - let parameter = self.parameter; - let parameter_id = parameter.id.clone(); + Block(block.max_transactions) => BlockParameter::MaxTransactions, - if !state_transaction.world.parameters.insert(parameter.clone()) { - return Err(RepetitionError { - instruction_type: InstructionType::NewParameter, - id: IdBox::ParameterId(parameter_id), - } - .into()); - } - state_transaction - .world - .emit_events(Some(ConfigurationEvent::Created(parameter_id))); - state_transaction.try_apply_core_parameter(parameter); + Transaction(transaction.max_instructions) => TransactionParameter::MaxInstructions, + Transaction(transaction.smart_contract_size) => TransactionParameter::SmartContractSize, + + SmartContract(smart_contract.fuel) => SmartContractParameter::Fuel, + SmartContract(smart_contract.memory) => SmartContractParameter::Memory, + + Executor(executor.fuel) => SmartContractParameter::Fuel, + Executor(executor.memory) => SmartContractParameter::Memory, + ); Ok(()) } @@ -407,7 +429,7 @@ pub mod isi { pub mod query { use eyre::Result; use iroha_data_model::{ - parameter::Parameter, + parameter::Parameters, peer::Peer, prelude::*, query::error::{FindError, QueryExecutionFail as Error}, @@ -485,11 +507,8 @@ pub mod query { impl ValidQuery for FindAllParameters { #[metrics("find_all_parameters")] - fn execute<'state>( - &self, - state_ro: &'state impl StateReadOnly, - ) -> Result + 'state>, Error> { - Ok(Box::new(state_ro.world().parameters_iter().cloned())) + fn execute(&self, state_ro: &impl StateReadOnly) -> Result { + Ok(state_ro.world().parameters().clone()) } } } diff --git a/core/src/smartcontracts/wasm.rs b/core/src/smartcontracts/wasm.rs index 9ab00dcbcf3..23f4612c9f5 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -2,17 +2,17 @@ //! `WebAssembly` VM Smartcontracts can be written in Rust, compiled //! to wasm format and submitted in a transaction -use std::borrow::Borrow; +use std::{borrow::Borrow, num::NonZeroU64}; use error::*; use import::traits::{ExecuteOperations as _, GetExecutorPayloads as _, SetDataModel as _}; -use iroha_config::parameters::actual::WasmRuntime as Config; use iroha_data_model::{ account::AccountId, executor::{self, ExecutorDataModel, MigrationResult}, isi::InstructionBox, + parameter::SmartContractParameters as Config, prelude::*, - query::{QueryBox, QueryId, QueryOutputBox, QueryRequest, SmartContractQuery}, + query::{cursor::QueryId, QueryBox, QueryOutputBox, QueryRequest, SmartContractQuery}, smart_contract::payloads::{self, Validate}, BatchedResponse, Level as LogLevel, ValidationFail, }; @@ -299,12 +299,12 @@ struct LimitsExecutor { /// Number of instructions in the smartcontract instruction_count: u64, /// Max allowed number of instructions in the smartcontract - max_instruction_count: u64, + max_instruction_count: NonZeroU64, } impl LimitsExecutor { /// Create new [`LimitsExecutor`] - pub fn new(max_instruction_count: u64) -> Self { + pub fn new(max_instruction_count: NonZeroU64) -> Self { Self { instruction_count: 0, max_instruction_count, @@ -320,7 +320,7 @@ impl LimitsExecutor { pub fn check_instruction_limits(&mut self) -> Result<(), ValidationFail> { self.instruction_count += 1; - if self.instruction_count > self.max_instruction_count { + if self.instruction_count > self.max_instruction_count.get() { return Err(ValidationFail::TooComplex); } @@ -344,8 +344,14 @@ 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: &Config) -> StoreLimits { + let memory_size = config + .memory + .get() + .try_into() + .expect("`SmarContractParameters::memory` exceeds usize::MAX"); + StoreLimitsBuilder::new() - .memory_size(config.max_memory.get() as usize) + .memory_size(memory_size) .instances(1) .memories(1) .tables(1) @@ -738,7 +744,7 @@ impl Runtime> { store.limiter(|s| &mut s.store_limits); store - .set_fuel(self.config.fuel_limit) + .set_fuel(self.config.fuel.get()) .expect("Wasm Runtime config is malformed, this is a bug"); store @@ -899,7 +905,7 @@ impl<'wrld, 'block: 'wrld, 'state: 'block> Runtime, authority: AccountId, bytes: impl AsRef<[u8]>, - max_instruction_count: u64, + max_instruction_count: NonZeroU64, ) -> Result<()> { let span = wasm_log_span!("Smart contract validation", %authority); let state = state::SmartContract::new( @@ -1706,6 +1712,7 @@ impl GetExport for (&wasmtime::Instance, C) { #[cfg(test)] mod tests { use iroha_data_model::query::{predicate::PredicateBox, sorting::Sorting, Pagination}; + use nonzero_ext::nonzero; use parity_scale_codec::Encode; use test_samples::gen_account_in; use tokio::test; @@ -1893,7 +1900,12 @@ mod tests { ); let mut runtime = RuntimeBuilder::::new().build()?; - let res = runtime.validate(&mut state.block().transaction(), authority, wat, 1); + let res = runtime.validate( + &mut state.block().transaction(), + authority, + wat, + nonzero!(1_u64), + ); if let Error::ExportFnCall(ExportFnCallError::Other(report)) = res.expect_err("Execution should fail") @@ -1942,7 +1954,12 @@ mod tests { ); let mut runtime = RuntimeBuilder::::new().build()?; - let res = runtime.validate(&mut state.block().transaction(), authority, wat, 1); + let res = runtime.validate( + &mut state.block().transaction(), + authority, + wat, + nonzero!(1_u64), + ); if let Error::ExportFnCall(ExportFnCallError::HostExecution(report)) = res.expect_err("Execution should fail") @@ -1986,7 +2003,12 @@ mod tests { ); let mut runtime = RuntimeBuilder::::new().build()?; - let res = runtime.validate(&mut state.block().transaction(), authority, wat, 1); + let res = runtime.validate( + &mut state.block().transaction(), + authority, + wat, + nonzero!(1_u64), + ); if let Error::ExportFnCall(ExportFnCallError::HostExecution(report)) = res.expect_err("Execution should fail") diff --git a/core/src/state.rs b/core/src/state.rs index 191d1b16261..e9c9e9f5a1c 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -1,15 +1,9 @@ //! This module provides the [`State`] — an in-memory representation of the current blockchain state. use std::{ - borrow::Borrow, - collections::BTreeSet, - marker::PhantomData, - num::{NonZeroU32, NonZeroUsize}, - sync::Arc, - time::Duration, + collections::BTreeSet, marker::PhantomData, num::NonZeroUsize, sync::Arc, time::Duration, }; use eyre::Result; -use iroha_config::{base::util::Bytes, parameters::actual::ChainWide as Config}; use iroha_crypto::HashOf; use iroha_data_model::{ account::AccountId, @@ -22,7 +16,7 @@ use iroha_data_model::{ }, executor::ExecutorDataModel, isi::error::{InstructionExecutionError as Error, MathError}, - parameter::{Parameter, ParameterValueBox}, + parameter::Parameters, permission::Permissions, prelude::*, query::error::{FindError, QueryExecutionFail}, @@ -62,14 +56,14 @@ use crate::{ wasm, Execute, }, tx::TransactionExecutor, - Parameters, PeersIds, + PeersIds, }; /// The global entity consisting of `domains`, `triggers` and etc. /// For example registration of domain, will have this as an ISI target. #[derive(Default, Serialize)] pub struct World { - /// Iroha config parameters. + /// Iroha on-chain parameters. pub(crate) parameters: Cell, /// Identifications of discovered trusted peers. pub(crate) trusted_peers_ids: Cell, @@ -93,8 +87,8 @@ pub struct World { /// Struct for block's aggregated changes pub struct WorldBlock<'world> { - /// Iroha config parameters. - pub(crate) parameters: CellBlock<'world, Parameters>, + /// Iroha on-chain parameters. + pub parameters: CellBlock<'world, Parameters>, /// Identifications of discovered trusted peers. pub(crate) trusted_peers_ids: CellBlock<'world, PeersIds>, /// Registered domains. @@ -119,7 +113,7 @@ pub struct WorldBlock<'world> { /// Struct for single transaction's aggregated changes pub struct WorldTransaction<'block, 'world> { - /// Iroha config parameters. + /// Iroha on-chain parameters. pub(crate) parameters: CellTransaction<'block, 'world, Parameters>, /// Identifications of discovered trusted peers. pub(crate) trusted_peers_ids: CellTransaction<'block, 'world, PeersIds>, @@ -153,7 +147,7 @@ struct TransactionEventBuffer<'block> { /// Consistent point in time view of the [`World`] pub struct WorldView<'world> { - /// Iroha config parameters. + /// Iroha on-chain parameters. pub(crate) parameters: CellView<'world, Parameters>, /// Identifications of discovered trusted peers. pub(crate) trusted_peers_ids: CellView<'world, PeersIds>, @@ -180,13 +174,11 @@ pub struct WorldView<'world> { pub struct State { /// 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: Cell, /// Blockchain. // TODO: Cell is redundant here since block_hashes is very easy to rollback by just popping the last element pub block_hashes: Cell>>, /// Hashes of transactions mapped onto block height where they stored - pub transactions: Storage, usize>, + pub transactions: Storage, NonZeroUsize>, /// Engine for WASM [`Runtime`](wasm::Runtime) to execute triggers. #[serde(skip)] pub engine: wasmtime::Engine, @@ -207,12 +199,10 @@ pub struct State { pub struct StateBlock<'state> { /// The world. Contains `domains`, `triggers`, `roles` and other data representing the current state of the blockchain. pub world: WorldBlock<'state>, - /// Configuration of World State View. - pub config: CellBlock<'state, Config>, /// Blockchain. pub block_hashes: CellBlock<'state, Vec>>, /// Hashes of transactions mapped onto block height where they stored - pub transactions: StorageBlock<'state, HashOf, usize>, + pub transactions: StorageBlock<'state, HashOf, NonZeroUsize>, /// Engine for WASM [`Runtime`](wasm::Runtime) to execute triggers. pub engine: &'state wasmtime::Engine, @@ -229,12 +219,10 @@ pub struct StateBlock<'state> { pub struct StateTransaction<'block, 'state> { /// The world. Contains `domains`, `triggers`, `roles` and other data representing the current state of the blockchain. pub world: WorldTransaction<'block, 'state>, - /// Configuration of World State View. - pub config: CellTransaction<'block, 'state, Config>, /// Blockchain. pub block_hashes: CellTransaction<'block, 'state, Vec>>, /// Hashes of transactions mapped onto block height where they stored - pub transactions: StorageTransaction<'block, 'state, HashOf, usize>, + pub transactions: StorageTransaction<'block, 'state, HashOf, NonZeroUsize>, /// Engine for WASM [`Runtime`](wasm::Runtime) to execute triggers. pub engine: &'state wasmtime::Engine, @@ -251,12 +239,10 @@ pub struct StateTransaction<'block, 'state> { pub struct StateView<'state> { /// The world. Contains `domains`, `triggers`, `roles` and other data representing the current state of the blockchain. pub world: WorldView<'state>, - /// Configuration of World State View. - pub config: CellView<'state, Config>, /// Blockchain. pub block_hashes: CellView<'state, Vec>>, /// Hashes of transactions mapped onto block height where they stored - pub transactions: StorageView<'state, HashOf, usize>, + pub transactions: StorageView<'state, HashOf, NonZeroUsize>, /// Engine for WASM [`Runtime`](wasm::Runtime) to execute triggers. pub engine: &'state wasmtime::Engine, @@ -289,11 +275,11 @@ impl World { .into_iter() .map(|account| (account.id().clone(), account)) .collect(); - World { + Self { trusted_peers_ids: Cell::new(trusted_peers_ids), domains, accounts, - ..World::new() + ..Self::new() } } @@ -331,7 +317,7 @@ impl World { } } - /// Create point in time view of the [`World`] + /// Create point in time view of the [`Self`] pub fn view(&self) -> WorldView { WorldView { parameters: self.parameters.view(), @@ -573,26 +559,6 @@ pub trait WorldReadOnly { self.trusted_peers_ids().iter() } - /// Get all `Parameter`s registered in the world. - fn parameters_iter(&self) -> impl Iterator { - self.parameters().iter() - } - - /// Query parameter and convert it to a proper type - fn query_param, P: core::hash::Hash + Eq + ?Sized>( - &self, - param: &P, - ) -> Option - where - Parameter: Borrow

    , - { - Parameters::get(self.parameters(), param) - .as_ref() - .map(|param| ¶m.val) - .cloned() - .and_then(|param_val| param_val.try_into().ok()) - } - /// Returns reference for trusted peer ids #[inline] fn peers_ids(&self) -> &PeersIds { @@ -748,7 +714,7 @@ impl WorldTransaction<'_, '_> { /// Remove all [`Role`]s from the [`Account`] pub fn remove_account_roles(&mut self, account: &AccountId) { let roles_to_remove = self - .account_roles_iter(&account) + .account_roles_iter(account) .cloned() .map(|role| RoleIdWithOwner::new(account.clone(), role.clone())) .collect::>(); @@ -899,7 +865,34 @@ impl WorldTransaction<'_, '_> { /// Set executor data model. pub fn set_executor_data_model(&mut self, executor_data_model: ExecutorDataModel) { - *self.executor_data_model.get_mut() = executor_data_model; + let prev_executor_data_model = + core::mem::replace(self.executor_data_model.get_mut(), executor_data_model); + + self.update_parameters(&prev_executor_data_model); + } + + fn update_parameters(&mut self, prev_executor_data_model: &ExecutorDataModel) { + let removed_parameters = prev_executor_data_model + .parameters + .keys() + .filter(|param_id| !self.executor_data_model.parameters.contains_key(param_id)); + let new_parameters = self + .executor_data_model + .parameters + .iter() + .filter(|(param_id, _)| !prev_executor_data_model.parameters.contains_key(param_id)); + + for param in removed_parameters { + iroha_logger::info!("{}: parameter removed", param); + self.parameters.custom.remove(param); + } + + for (param_id, param) in new_parameters { + self.parameters + .custom + .insert(param_id.clone(), param.clone()); + iroha_logger::info!("{}: parameter created", param); + } } /// Execute trigger with `trigger_id` as id and `authority` as owner @@ -978,21 +971,8 @@ impl State { #[must_use] #[inline] pub fn new(world: World, kura: Arc, query_handle: LiveQueryStoreHandle) -> Self { - // Added to remain backward compatible with other code primary in tests - Self::from_config(Config::default(), world, kura, query_handle) - } - - /// Construct [`State`] with specific [`Configuration`]. - #[inline] - pub fn from_config( - config: Config, - world: World, - kura: Arc, - query_handle: LiveQueryStoreHandle, - ) -> Self { Self { world, - config: Cell::new(config), transactions: Storage::new(), block_hashes: Cell::new(Vec::new()), new_tx_amounts: Arc::new(Mutex::new(Vec::new())), @@ -1006,7 +986,6 @@ impl State { pub fn block(&self) -> StateBlock<'_> { StateBlock { world: self.world.block(), - config: self.config.block(), block_hashes: self.block_hashes.block(), transactions: self.transactions.block(), engine: &self.engine, @@ -1020,7 +999,6 @@ impl State { pub fn block_and_revert(&self) -> StateBlock<'_> { StateBlock { world: self.world.block_and_revert(), - config: self.config.block_and_revert(), block_hashes: self.block_hashes.block_and_revert(), transactions: self.transactions.block_and_revert(), engine: &self.engine, @@ -1034,7 +1012,6 @@ impl State { pub fn view(&self) -> StateView<'_> { StateView { world: self.world.view(), - config: self.config.view(), block_hashes: self.block_hashes.view(), transactions: self.transactions.view(), engine: &self.engine, @@ -1049,9 +1026,8 @@ impl State { #[allow(missing_docs)] pub trait StateReadOnly { fn world(&self) -> &impl WorldReadOnly; - fn config(&self) -> &Config; fn block_hashes(&self) -> &[HashOf]; - fn transactions(&self) -> &impl StorageReadOnly, usize>; + fn transactions(&self) -> &impl StorageReadOnly, NonZeroUsize>; fn engine(&self) -> &wasmtime::Engine; fn kura(&self) -> &Kura; fn query_handle(&self) -> &LiveQueryStoreHandle; @@ -1121,8 +1097,7 @@ pub trait StateReadOnly { fn block_with_tx(&self, hash: &HashOf) -> Option> { self.transactions() .get(hash) - .and_then(|&height| NonZeroUsize::new(height)) - .and_then(|height| self.kura().get_block_by_height(height)) + .and_then(|&height| self.kura().get_block_by_height(height)) } /// Returns [`Some`] milliseconds since the genesis block was @@ -1153,7 +1128,7 @@ pub trait StateReadOnly { /// Get transaction executor fn transaction_executor(&self) -> TransactionExecutor { - TransactionExecutor::new(self.config().transaction_limits) + TransactionExecutor::new(self.world().parameters().transaction) } } @@ -1163,13 +1138,10 @@ macro_rules! impl_state_ro { fn world(&self) -> &impl WorldReadOnly { &self.world } - fn config(&self) -> &Config { - &self.config - } fn block_hashes(&self) -> &[HashOf] { &self.block_hashes } - fn transactions(&self) -> &impl StorageReadOnly, usize> { + fn transactions(&self) -> &impl StorageReadOnly, NonZeroUsize> { &self.transactions } fn engine(&self) -> &wasmtime::Engine { @@ -1197,7 +1169,6 @@ impl<'state> StateBlock<'state> { pub fn transaction(&mut self) -> StateTransaction<'_, 'state> { StateTransaction { world: self.world.trasaction(), - config: self.config.transaction(), block_hashes: self.block_hashes.transaction(), transactions: self.transactions.transaction(), engine: self.engine, @@ -1211,7 +1182,6 @@ impl<'state> StateBlock<'state> { pub fn commit(self) { self.transactions.commit(); self.block_hashes.commit(); - self.config.commit(); self.world.commit(); } @@ -1387,101 +1357,9 @@ impl StateTransaction<'_, '_> { pub fn apply(self) { self.transactions.apply(); self.block_hashes.apply(); - self.config.apply(); self.world.apply(); } - /// If given [`Parameter`] represents some of the core chain-wide - /// parameters ([`Config`]), apply it - pub fn try_apply_core_parameter(&mut self, parameter: Parameter) { - use iroha_data_model::parameter::default::*; - - struct Reader(Option); - - impl Reader { - fn try_and_then>( - self, - id: &str, - fun: impl FnOnce(T), - ) -> Self { - if let Some(param) = self.0 { - if param.id().name().as_ref() == id { - if let Ok(value) = param.val.try_into() { - fun(value); - } - Self(None) - } else { - Self(Some(param)) - } - } else { - Self(None) - } - } - - fn try_and_write>( - self, - id: &str, - destination: &mut T, - ) -> Self { - self.try_and_then(id, |value| { - *destination = value; - }) - } - - fn try_and_write_duration(self, id: &str, destination: &mut Duration) -> Self { - self.try_and_then(id, |value| *destination = Duration::from_millis(value)) - } - - fn try_and_write_bytes(self, id: &str, destination: &mut Bytes) -> Self { - self.try_and_then(id, |value| *destination = Bytes(value)) - } - } - - Reader(Some(parameter)) - .try_and_then(MAX_TRANSACTIONS_IN_BLOCK, |value| { - if let Some(checked) = NonZeroU32::new(value) { - self.config.max_transactions_in_block = checked; - } - }) - .try_and_write_duration(BLOCK_TIME, &mut self.config.block_time) - .try_and_write_duration(COMMIT_TIME_LIMIT, &mut self.config.commit_time) - .try_and_write( - WSV_DOMAIN_METADATA_LIMITS, - &mut self.config.domain_metadata_limits, - ) - .try_and_write( - WSV_ASSET_DEFINITION_METADATA_LIMITS, - &mut self.config.asset_definition_metadata_limits, - ) - .try_and_write( - WSV_ACCOUNT_METADATA_LIMITS, - &mut self.config.account_metadata_limits, - ) - .try_and_write( - WSV_ASSET_METADATA_LIMITS, - &mut self.config.asset_metadata_limits, - ) - .try_and_write( - WSV_TRIGGER_METADATA_LIMITS, - &mut self.config.trigger_metadata_limits, - ) - .try_and_write( - WSV_IDENT_LENGTH_LIMITS, - &mut self.config.ident_length_limits, - ) - .try_and_write( - EXECUTOR_FUEL_LIMIT, - &mut self.config.executor_runtime.fuel_limit, - ) - .try_and_write_bytes( - EXECUTOR_MAX_MEMORY, - &mut self.config.executor_runtime.max_memory, - ) - .try_and_write(WASM_FUEL_LIMIT, &mut self.config.wasm_runtime.fuel_limit) - .try_and_write_bytes(WASM_MAX_MEMORY, &mut self.config.wasm_runtime.max_memory) - .try_and_write(TRANSACTION_LIMITS, &mut self.config.transaction_limits); - } - fn process_executable(&mut self, executable: &Executable, authority: AccountId) -> Result<()> { match executable { Executable::Instructions(instructions) => { @@ -1489,7 +1367,7 @@ impl StateTransaction<'_, '_> { } Executable::Wasm(bytes) => { let mut wasm_runtime = wasm::RuntimeBuilder::::new() - .with_config(self.config.wasm_runtime) + .with_config(self.world().parameters().smart_contract) .with_engine(self.engine.clone()) // Cloning engine is cheap .build()?; wasm_runtime @@ -1531,7 +1409,7 @@ impl StateTransaction<'_, '_> { .expect("INTERNAL BUG: contract is not present") .clone(); let mut wasm_runtime = wasm::RuntimeBuilder::::new() - .with_config(self.config.wasm_runtime) + .with_config(self.world().parameters().smart_contract) .with_engine(self.engine.clone()) // Cloning engine is cheap .build()?; wasm_runtime @@ -1877,7 +1755,6 @@ pub(crate) mod deserialize { M: MapAccess<'de>, { let mut world = None; - let mut config = None; let mut block_hashes = None; let mut transactions = None; @@ -1893,9 +1770,6 @@ pub(crate) mod deserialize { "world" => { world = Some(map.next_value_seed(wasm_seed.cast::())?); } - "config" => { - config = Some(map.next_value()?); - } "block_hashes" => { block_hashes = Some(map.next_value()?); } @@ -1908,7 +1782,6 @@ pub(crate) mod deserialize { Ok(State { world: world.ok_or_else(|| serde::de::Error::missing_field("world"))?, - config: config.ok_or_else(|| serde::de::Error::missing_field("config"))?, block_hashes: block_hashes .ok_or_else(|| serde::de::Error::missing_field("block_hashes"))?, transactions: transactions @@ -1923,7 +1796,7 @@ pub(crate) mod deserialize { deserializer.deserialize_struct( "WorldState", - &["world", "config", "block_hashes", "transactions"], + &["world", "block_hashes", "transactions"], StateVisitor { loader: self }, ) } @@ -1932,6 +1805,8 @@ pub(crate) mod deserialize { #[cfg(test)] mod tests { + use core::num::NonZeroU64; + use iroha_data_model::block::BlockPayload; use test_samples::gen_account_in; @@ -1965,7 +1840,7 @@ mod tests { let mut block_hashes = vec![]; for i in 1..=BLOCK_CNT { let block = new_dummy_block_with_payload(|payload| { - payload.header.height = i as u64; + payload.header.height = NonZeroU64::new(i as u64).unwrap(); payload.header.prev_block_hash = block_hashes.last().copied(); }); @@ -1990,7 +1865,7 @@ mod tests { for i in 1..=BLOCK_CNT { let block = new_dummy_block_with_payload(|payload| { - payload.header.height = i as u64; + payload.header.height = NonZeroU64::new(i as u64).unwrap(); }); let _events = state_block.apply(&block).unwrap(); @@ -2001,7 +1876,7 @@ mod tests { &state_block .all_blocks() .skip(7) - .map(|block| block.header().height()) + .map(|block| block.header().height().get()) .collect::>(), &[8, 9, 10] ); diff --git a/core/src/sumeragi/main_loop.rs b/core/src/sumeragi/main_loop.rs index 856b94e4d78..f8c2d8dd17b 100644 --- a/core/src/sumeragi/main_loop.rs +++ b/core/src/sumeragi/main_loop.rs @@ -21,14 +21,6 @@ pub struct Sumeragi { pub peer_id: PeerId, /// An actor that sends events pub events_sender: EventsSender, - /// Time by which a newly created block should be committed. Prevents malicious nodes - /// from stalling the network by not participating in consensus - pub commit_time: Duration, - /// Time by which a new block should be created regardless if there were enough transactions or not. - /// Used to force block commits when there is a small influx of new transactions. - pub block_time: Duration, - /// The maximum number of transactions in the block - pub max_txs_in_block: usize, /// Kura instance used for IO pub kura: Arc, /// [`iroha_p2p::Network`] actor address @@ -122,12 +114,6 @@ impl Sumeragi { self.network.update_topology(UpdateTopology(peers)); } - /// The maximum time a sumeragi round can take to produce a block when - /// there are no faulty peers in the a set. - fn pipeline_time(&self) -> Duration { - self.block_time + self.commit_time - } - fn send_event(&self, event: impl Into) { let _ = self.events_sender.send(event.into()); } @@ -347,8 +333,6 @@ impl Sumeragi { let state_events = state_block.apply_without_execution(&block); - // Parameters are updated before updating public copy of sumeragi - self.update_params(&state_block); self.cache_transaction(&state_block); self.topology @@ -385,12 +369,6 @@ impl Sumeragi { self.was_commit = true; } - fn update_params(&mut self, state_block: &StateBlock<'_>) { - self.block_time = state_block.config.block_time; - self.commit_time = state_block.config.commit_time; - self.max_txs_in_block = state_block.config.max_transactions_in_block.get() as usize; - } - fn cache_transaction(&mut self, state_block: &StateBlock<'_>) { self.transaction_cache.retain(|tx| { !state_block.has_transaction(tx.as_ref().hash()) && !self.queue.is_expired(tx) @@ -406,7 +384,7 @@ impl Sumeragi { ) -> Option> { let mut state_block = state.block(); - if state_block.height() == 1 && block.header().height == 1 { + if state_block.height() == 1 && block.header().height.get() == 1 { // Consider our peer has genesis, // and some other peer has genesis and broadcast it to our peer, // then we can ignore such genesis block because we already has genesis. @@ -818,7 +796,14 @@ impl Sumeragi { #[cfg(debug_assertions)] if is_genesis_peer && self.debug_force_soft_fork { - std::thread::sleep(self.pipeline_time() * 2); + let pipeline_time = voting_block + .state_block + .world + .parameters() + .sumeragi + .pipeline_time(); + + std::thread::sleep(pipeline_time * 2); } else { let msg = BlockCommitted::from(&committed_block); self.broadcast_packet(msg); @@ -846,8 +831,17 @@ impl Sumeragi { ) { assert_eq!(self.role(), Role::Leader); - let tx_cache_full = self.transaction_cache.len() >= self.max_txs_in_block; - let deadline_reached = self.round_start_time.elapsed() > self.block_time; + let max_transactions: NonZeroUsize = state + .world + .view() + .parameters + .block + .max_transactions + .try_into() + .expect("INTERNAL BUG: transactions in block exceed usize::MAX"); + let block_time = state.world.view().parameters.sumeragi.block_time(); + let tx_cache_full = self.transaction_cache.len() >= max_transactions.get(); + let deadline_reached = self.round_start_time.elapsed() > block_time; let tx_cache_non_empty = !self.transaction_cache.is_empty(); if tx_cache_full || (deadline_reached && tx_cache_non_empty) { @@ -864,7 +858,8 @@ impl Sumeragi { .unpack(|e| self.send_event(e)); let created_in = create_block_start_time.elapsed(); - if created_in > self.pipeline_time() / 2 { + let pipeline_time = state.world.view().parameters().sumeragi.pipeline_time(); + if created_in > pipeline_time / 2 { warn!( role=%self.role(), peer_id=%self.peer_id, @@ -1010,7 +1005,7 @@ pub(crate) fn run( let mut should_sleep = false; let mut view_change_proof_chain = ProofChain::default(); // Duration after which a view change is suggested - let mut view_change_time = sumeragi.pipeline_time(); + let mut view_change_time = state.world.view().parameters().sumeragi.pipeline_time(); // Instant when the previous view change or round happened. let mut last_view_change_time = Instant::now(); @@ -1040,7 +1035,14 @@ pub(crate) fn run( sumeragi.queue.get_transactions_for_block( &state_view, - sumeragi.max_txs_in_block, + state + .world + .view() + .parameters + .block + .max_transactions + .try_into() + .expect("INTERNAL BUG: transactions in block exceed usize::MAX"), &mut sumeragi.transaction_cache, ); @@ -1053,7 +1055,7 @@ pub(crate) fn run( reset_state( &sumeragi.peer_id, - sumeragi.pipeline_time(), + state.world.view().parameters().sumeragi.pipeline_time(), view_change_index, &mut sumeragi.was_commit, &mut sumeragi.topology, @@ -1138,12 +1140,12 @@ pub(crate) fn run( // NOTE: View change must be periodically suggested until it is accepted. // Must be initialized to pipeline time but can increase by chosen amount - view_change_time += sumeragi.pipeline_time(); + view_change_time += state.world.view().parameters().sumeragi.pipeline_time(); } reset_state( &sumeragi.peer_id, - sumeragi.pipeline_time(), + state.world.view().parameters().sumeragi.pipeline_time(), view_change_index, &mut sumeragi.was_commit, &mut sumeragi.topology, @@ -1235,7 +1237,7 @@ enum BlockSyncError { }, BlockNotProperHeight { peer_height: usize, - block_height: usize, + block_height: NonZeroUsize, }, } @@ -1246,18 +1248,18 @@ fn handle_block_sync<'state, F: Fn(PipelineEventBox)>( genesis_account: &AccountId, handle_events: &F, ) -> Result, (SignedBlock, BlockSyncError)> { - let block_height = block + let block_height: NonZeroUsize = block .header() .height .try_into() .expect("INTERNAL BUG: Block height exceeds usize::MAX"); let state_height = state.view().height(); - let (mut state_block, soft_fork) = if state_height + 1 == block_height { + let (mut state_block, soft_fork) = if state_height + 1 == block_height.get() { // NOTE: Normal branch for adding new block on top of current (state.block(), false) - } else if state_height == block_height && block_height > 1 { + } else if state_height == block_height.get() && block_height.get() > 1 { // NOTE: Soft fork branch for replacing current block with valid one let latest_block = state @@ -1330,6 +1332,7 @@ fn handle_block_sync<'state, F: Fn(PipelineEventBox)>( #[cfg(test)] mod tests { use iroha_genesis::GENESIS_DOMAIN_ID; + use nonzero_ext::nonzero; use test_samples::gen_account_in; use tokio::test; @@ -1376,12 +1379,9 @@ mod tests { let tx = TransactionBuilder::new(chain_id.clone(), alice_id.clone()) .with_instructions([fail_isi]) .sign(alice_keypair.private_key()); - let tx = AcceptedTransaction::accept( - tx, - chain_id, - state_block.transaction_executor().transaction_limits, - ) - .expect("Valid"); + let tx = + AcceptedTransaction::accept(tx, chain_id, state_block.transaction_executor().limits) + .expect("Valid"); // Creating a block of two identical transactions and validating it let block = BlockBuilder::new(vec![tx.clone(), tx], topology.clone(), Vec::new()) @@ -1413,7 +1413,7 @@ mod tests { let tx1 = AcceptedTransaction::accept( tx1, chain_id, - state_block.transaction_executor().transaction_limits, + state_block.transaction_executor().limits, ) .map(Into::into) .expect("Valid"); @@ -1423,7 +1423,7 @@ mod tests { let tx2 = AcceptedTransaction::accept( tx2, chain_id, - state_block.transaction_executor().transaction_limits, + state_block.transaction_executor().limits, ) .map(Into::into) .expect("Valid"); @@ -1511,20 +1511,26 @@ mod tests { // Change block height let block = clone_and_modify_payload(&block, &leader_private_key, |payload| { - payload.header.height = 42; + payload.header.height = nonzero!(42_u64); }); let result = handle_block_sync(&chain_id, block, &state, &genesis_public_key, &|_| {}); + assert!(matches!( result, - Err(( - _, - BlockSyncError::BlockNotProperHeight { - peer_height: 1, - block_height: 42 - } - )) - )) + Err((_, BlockSyncError::BlockNotProperHeight { .. })) + )); + if let Err(( + _, + BlockSyncError::BlockNotProperHeight { + peer_height, + block_height, + }, + )) = result + { + assert_eq!(peer_height, 1); + assert_eq!(block_height, nonzero!(42_usize)); + } } #[test] @@ -1655,19 +1661,25 @@ mod tests { // Soft-fork on genesis block is not possible let block = clone_and_modify_payload(&block, &leader_private_key, |payload| { payload.header.view_change_index = 42; - payload.header.height = 1; + payload.header.height = nonzero!(1_u64); }); let result = handle_block_sync(&chain_id, block, &state, &genesis_public_key, &|_| {}); + assert!(matches!( result, - Err(( - _, - BlockSyncError::BlockNotProperHeight { - peer_height: 1, - block_height: 1, - } - )) - )) + Err((_, BlockSyncError::BlockNotProperHeight { .. })) + )); + if let Err(( + _, + BlockSyncError::BlockNotProperHeight { + peer_height, + block_height, + }, + )) = result + { + assert_eq!(peer_height, 1); + assert_eq!(block_height, nonzero!(1_usize)); + } } } diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 9763fe7324c..42a7921a617 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -211,9 +211,6 @@ impl SumeragiHandle { peer_id: peer_id.clone(), queue: Arc::clone(&queue), events_sender, - commit_time: state.view().config.commit_time, - block_time: state.view().config.block_time, - max_txs_in_block: state.view().config.max_transactions_in_block.get() as usize, kura: Arc::clone(&kura), network: network.clone(), control_message_receiver, diff --git a/core/src/tx.rs b/core/src/tx.rs index 6b9f04413ed..2b841f7bba2 100644 --- a/core/src/tx.rs +++ b/core/src/tx.rs @@ -14,7 +14,7 @@ pub use iroha_data_model::prelude::*; use iroha_data_model::{ isi::error::Mismatch, query::error::FindError, - transaction::{error::TransactionLimitError, TransactionLimits, TransactionPayload}, + transaction::{error::TransactionLimitError, TransactionPayload}, }; use iroha_logger::{debug, error}; use iroha_macro::FromVariant; @@ -95,7 +95,7 @@ impl AcceptedTransaction { pub fn accept( tx: SignedTransaction, expected_chain_id: &ChainId, - limits: TransactionLimits, + limits: TransactionParameters, ) -> Result { let actual_chain_id = tx.chain(); @@ -112,13 +112,19 @@ impl AcceptedTransaction { match &tx.instructions() { Executable::Instructions(instructions) => { - let instruction_count = instructions.len(); - if Self::len_u64(instruction_count) > limits.max_instruction_number { + let instruction_limit = limits + .max_instructions + .get() + .try_into() + .expect("INTERNAL BUG: max instructions exceeds usize::MAX"); + + if instructions.len() > instruction_limit { return Err(AcceptTransactionFail::TransactionLimit( TransactionLimitError { reason: format!( "Too many instructions in payload, max number is {}, but got {}", - limits.max_instruction_number, instruction_count + limits.max_instructions, + instructions.len() ), }, )); @@ -129,13 +135,21 @@ impl AcceptedTransaction { // // Should we allow infinite instructions in wasm? And deny only based on fuel and size Executable::Wasm(smart_contract) => { - let size_bytes = Self::len_u64(smart_contract.size_bytes()); - let max_wasm_size_bytes = limits.max_wasm_size_bytes; + let smart_contract_size_limit = limits + .smart_contract_size + .get() + .try_into() + .expect("INTERNAL BUG: smart contract size exceeds usize::MAX"); - if size_bytes > max_wasm_size_bytes { + if smart_contract.size_bytes() > smart_contract_size_limit { return Err(AcceptTransactionFail::TransactionLimit( TransactionLimitError { - reason: format!("Wasm binary too large, max size is {max_wasm_size_bytes}, but got {size_bytes}"), + reason: format!( + "WASM binary size is too large: max {}, got {} \ + (configured by \"Parameter::SmartContractLimits\")", + limits.smart_contract_size, + smart_contract.size_bytes() + ), }, )); } @@ -144,11 +158,6 @@ impl AcceptedTransaction { Ok(Self(tx)) } - - #[inline] - fn len_u64(instruction_count: usize) -> u64 { - u64::try_from(instruction_count).expect("`usize` should always fit into `u64`") - } } impl From for SignedTransaction { @@ -174,14 +183,16 @@ impl AsRef for AcceptedTransaction { /// Validation is skipped for genesis. #[derive(Clone, Copy)] pub struct TransactionExecutor { - /// [`TransactionLimits`] field - pub transaction_limits: TransactionLimits, + /// [`TransactionParameters`] field + pub limits: TransactionParameters, } impl TransactionExecutor { /// Construct [`TransactionExecutor`] - pub fn new(transaction_limits: TransactionLimits) -> Self { - Self { transaction_limits } + pub fn new(transaction_limits: TransactionParameters) -> Self { + Self { + limits: transaction_limits, + } } /// Move transaction lifecycle forward by checking if the @@ -244,7 +255,7 @@ impl TransactionExecutor { state_transaction, authority, wasm, - self.transaction_limits.max_instruction_number, + self.limits.max_instructions, ) }) .map_err(|error| WasmExecutionFail { diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index 61d30429024..54e4fc01dc3 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -665,13 +665,9 @@ 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 config = self.config.clone().unwrap_or_else(Config::test); - let peer = self.start().await; - let client = Client::test(&peer.api_address); - - time::sleep(config.chain_wide.pipeline_time()).await; + time::sleep(::pipeline_time()).await; (peer, client) } @@ -818,7 +814,8 @@ impl TestConfig for Config { } fn pipeline_time() -> Duration { - Self::test().chain_wide.pipeline_time() + let defaults = iroha_data_model::parameter::SumeragiParameters::default(); + defaults.block_time() + defaults.commit_time() } fn block_sync_gossip_time() -> Duration { diff --git a/data_model/derive/src/id.rs b/data_model/derive/src/id.rs index c9a64c64537..baaa45daa31 100644 --- a/data_model/derive/src/id.rs +++ b/data_model/derive/src/id.rs @@ -85,29 +85,29 @@ pub fn impl_id_eq_ord_hash(emitter: &mut Emitter, input: &syn::DeriveInput) -> T quote! { #identifiable_derive - impl #impl_generics ::core::cmp::PartialOrd for #name #ty_generics #where_clause where Self: Identifiable { + impl #impl_generics ::core::cmp::PartialOrd for #name #ty_generics #where_clause where Self: crate::Identifiable { #[inline] fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> { Some(self.cmp(other)) } } - impl #impl_generics ::core::cmp::Ord for #name #ty_generics #where_clause where Self: Identifiable { + impl #impl_generics ::core::cmp::Ord for #name #ty_generics #where_clause where Self: crate::Identifiable { fn cmp(&self, other: &Self) -> ::core::cmp::Ordering { - self.id().cmp(other.id()) + ::id(self).cmp(::id(other)) } } - impl #impl_generics ::core::cmp::Eq for #name #ty_generics #where_clause where Self: Identifiable {} - impl #impl_generics ::core::cmp::PartialEq for #name #ty_generics #where_clause where Self: Identifiable { + impl #impl_generics ::core::cmp::Eq for #name #ty_generics #where_clause where Self: crate::Identifiable {} + impl #impl_generics ::core::cmp::PartialEq for #name #ty_generics #where_clause where Self: crate::Identifiable { fn eq(&self, other: &Self) -> bool { - self.id() == other.id() + ::id(self) == ::id(other) } } - impl #impl_generics ::core::hash::Hash for #name #ty_generics #where_clause where Self: Identifiable { + impl #impl_generics ::core::hash::Hash for #name #ty_generics #where_clause where Self: crate::Identifiable { fn hash(&self, state: &mut H) { - self.id().hash(state); + ::id(self).hash(state) } } } @@ -119,7 +119,7 @@ fn derive_identifiable(emitter: &mut Emitter, input: &IdDeriveInput) -> TokenStr let (id_type, id_expr) = get_id_type(emitter, input); quote! { - impl #impl_generics Identifiable for #name #ty_generics #where_clause { + impl #impl_generics crate::Identifiable for #name #ty_generics #where_clause { type Id = #id_type; #[inline] @@ -142,8 +142,8 @@ fn get_id_type(emitter: &mut Emitter, input: &IdDeriveInput) -> (syn::Type, syn: } IdAttr::Transparent => { return ( - parse_quote! {<#ty as Identifiable>::Id}, - parse_quote! {Identifiable::id(&self.#field_name)}, + parse_quote! {<#ty as crate::Identifiable>::Id}, + parse_quote! {crate::Identifiable::id(&self.#field_name)}, ); } IdAttr::Missing => { diff --git a/data_model/derive/tests/has_origin_generics.rs b/data_model/derive/tests/has_origin_generics.rs index a1090a312cc..69724714bb1 100644 --- a/data_model/derive/tests/has_origin_generics.rs +++ b/data_model/derive/tests/has_origin_generics.rs @@ -16,12 +16,6 @@ struct Object { id: ObjectId, } -impl Object { - fn id(&self) -> &ObjectId { - &self.id - } -} - #[allow(clippy::enum_variant_names)] // it's a test, duh #[derive(Debug, HasOrigin)] #[has_origin(origin = Object)] diff --git a/data_model/src/account.rs b/data_model/src/account.rs index 0e99bf6d495..c8daf5c8cb3 100644 --- a/data_model/src/account.rs +++ b/data_model/src/account.rs @@ -4,7 +4,6 @@ use alloc::{format, string::String, vec::Vec}; use core::str::FromStr; use derive_more::{Constructor, DebugCustom, Display}; -use getset::Getters; use iroha_data_model_derive::{model, IdEqOrdHash}; use iroha_schema::IntoSchema; use parity_scale_codec::{Decode, Encode}; @@ -16,11 +15,13 @@ use crate::{ asset::{Asset, AssetDefinitionId, AssetsMap}, domain::prelude::*, metadata::Metadata, - HasMetadata, Identifiable, ParseError, PublicKey, Registered, + HasMetadata, ParseError, PublicKey, Registered, }; #[model] mod model { + use getset::Getters; + use super::*; /// Identification of [`Account`] by the combination of the [`PublicKey`] as its sole signatory and the [`Domain`](crate::domain::Domain) it belongs to. @@ -66,16 +67,7 @@ mod model { /// Account entity is an authority which is used to execute `Iroha Special Instructions`. #[derive( - Debug, - Display, - Clone, - IdEqOrdHash, - Getters, - Decode, - Encode, - Deserialize, - Serialize, - IntoSchema, + Debug, Display, Clone, IdEqOrdHash, Decode, Encode, Deserialize, Serialize, IntoSchema, )] #[allow(clippy::multiple_inherent_impl)] #[display(fmt = "({id})")] // TODO: Add more? @@ -234,10 +226,10 @@ pub mod prelude { } #[cfg(test)] +#[cfg(feature = "transparent_api")] mod tests { use super::*; - #[cfg(feature = "transparent_api")] #[test] fn parse_account_id() { const SIGNATORY: &str = diff --git a/data_model/src/asset.rs b/data_model/src/asset.rs index 6b3972c1e49..750f261f577 100644 --- a/data_model/src/asset.rs +++ b/data_model/src/asset.rs @@ -7,7 +7,6 @@ use core::{fmt, str::FromStr}; use std::collections::btree_map; use derive_more::{Constructor, DebugCustom, Display}; -use getset::{CopyGetters, Getters}; use iroha_data_model_derive::{model, IdEqOrdHash}; use iroha_primitives::numeric::{Numeric, NumericSpec, NumericSpecParseError}; use iroha_schema::IntoSchema; @@ -17,8 +16,8 @@ use serde_with::{DeserializeFromStr, SerializeDisplay}; pub use self::model::*; use crate::{ - account::prelude::*, domain::prelude::*, ipfs::IpfsPath, metadata::Metadata, HasMetadata, - Identifiable, Name, ParseError, Registered, + account::prelude::*, domain::prelude::*, ipfs::IpfsPath, metadata::Metadata, HasMetadata, Name, + ParseError, Registered, }; /// API to work with collections of [`Id`] : [`Asset`] mappings. @@ -34,6 +33,7 @@ pub type AssetTotalQuantityMap = btree_map::BTreeMap #[model] mod model { + use getset::{CopyGetters, Getters}; use iroha_macro::FromVariant; use super::*; diff --git a/data_model/src/block.rs b/data_model/src/block.rs index 6e7c5814519..5a17d163512 100644 --- a/data_model/src/block.rs +++ b/data_model/src/block.rs @@ -14,6 +14,7 @@ use iroha_data_model_derive::model; use iroha_macro::FromVariant; use iroha_schema::IntoSchema; use iroha_version::{declare_versioned, version_with_scale}; +use nonzero_ext::nonzero; use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; @@ -22,6 +23,8 @@ use crate::{events::prelude::*, peer, peer::PeerId, transaction::prelude::*}; #[model] mod model { + use core::num::NonZeroU64; + use getset::{CopyGetters, Getters}; use super::*; @@ -52,7 +55,7 @@ mod model { pub struct BlockHeader { /// Number of blocks in the chain including this block. #[getset(get_copy = "pub")] - pub height: u64, + pub height: NonZeroU64, /// Hash of the previous block in the chain. #[getset(get_copy = "pub")] pub prev_block_hash: Option>, @@ -143,7 +146,7 @@ impl BlockHeader { #[inline] #[cfg(feature = "transparent_api")] pub const fn is_genesis(&self) -> bool { - self.height == 1 + self.height.get() == 1 } /// Creation timestamp @@ -294,7 +297,7 @@ impl SignedBlock { let timestamp_ms = u64::try_from(first_transaction.creation_time().as_millis()) .expect("Must fit since Duration was created from u64 in creation_time()"); let header = BlockHeader { - height: 1, + height: nonzero!(1_u64), prev_block_hash: None, transactions_hash, timestamp_ms, @@ -345,7 +348,7 @@ mod candidate { fn validate(self) -> Result { self.validate_signatures()?; self.validate_header()?; - if self.payload.header.height == 1 { + if self.payload.header.height.get() == 1 { self.validate_genesis()?; } @@ -390,7 +393,7 @@ mod candidate { } fn validate_signatures(&self) -> Result<(), &'static str> { - if self.signatures.is_empty() && self.payload.header.height != 1 { + if self.signatures.is_empty() && self.payload.header.height.get() != 1 { return Err("Block missing signatures"); } diff --git a/data_model/src/domain.rs b/data_model/src/domain.rs index db01cd686c1..fba99212f45 100644 --- a/data_model/src/domain.rs +++ b/data_model/src/domain.rs @@ -4,7 +4,6 @@ use alloc::{format, string::String, vec::Vec}; use derive_more::{Constructor, Display, FromStr}; -use getset::Getters; use iroha_data_model_derive::{model, IdEqOrdHash}; use iroha_primitives::numeric::Numeric; use iroha_schema::IntoSchema; @@ -23,6 +22,8 @@ use crate::{ #[model] mod model { + use getset::Getters; + use super::*; /// Identification of a [`Domain`]. diff --git a/data_model/src/events/data/events.rs b/data_model/src/events/data/events.rs index bf337d632d9..4734e34b27f 100644 --- a/data_model/src/events/data/events.rs +++ b/data_model/src/events/data/events.rs @@ -128,16 +128,16 @@ mod asset { pub enum AssetDefinitionEvent { #[has_origin(asset_definition => asset_definition.id())] Created(AssetDefinition), - MintabilityChanged(AssetDefinitionId), - #[has_origin(ownership_changed => &ownership_changed.asset_definition)] - OwnerChanged(AssetDefinitionOwnerChanged), - Deleted(AssetDefinitionId), + Delete(AssetDefinitionId), #[has_origin(metadata_changed => &metadata_changed.target)] MetadataInserted(AssetDefinitionMetadataChanged), #[has_origin(metadata_changed => &metadata_changed.target)] MetadataRemoved(AssetDefinitionMetadataChanged), + MintabilityChanged(AssetDefinitionId), #[has_origin(total_quantity_changed => &total_quantity_changed.asset_definition)] TotalQuantityChanged(AssetDefinitionTotalQuantityChanged), + #[has_origin(ownership_changed => &ownership_changed.asset_definition)] + OwnerChanged(AssetDefinitionOwnerChanged), } } @@ -244,13 +244,13 @@ mod role { Created(Role), Deleted(RoleId), /// [`Permission`]s with particular [`PermissionId`] - /// were removed from the role. - #[has_origin(permission_removed => &permission_removed.role)] - PermissionRemoved(RolePermissionChanged), - /// [`Permission`]s with particular [`PermissionId`] /// were removed added to the role. #[has_origin(permission_added => &permission_added.role)] PermissionAdded(RolePermissionChanged), + /// [`Permission`]s with particular [`PermissionId`] + /// were removed from the role. + #[has_origin(permission_removed => &permission_removed.role)] + PermissionRemoved(RolePermissionChanged), } } @@ -297,21 +297,19 @@ mod account { data_event! { #[has_origin(origin = Account)] pub enum AccountEvent { - #[has_origin(asset_event => &asset_event.origin().account)] - Asset(AssetEvent), #[has_origin(account => account.id())] Created(Account), Deleted(AccountId), - AuthenticationAdded(AccountId), - AuthenticationRemoved(AccountId), + #[has_origin(asset_event => &asset_event.origin().account)] + Asset(AssetEvent), #[has_origin(permission_changed => &permission_changed.account)] PermissionAdded(AccountPermissionChanged), #[has_origin(permission_changed => &permission_changed.account)] PermissionRemoved(AccountPermissionChanged), #[has_origin(role_changed => &role_changed.account)] - RoleRevoked(AccountRoleChanged), - #[has_origin(role_changed => &role_changed.account)] RoleGranted(AccountRoleChanged), + #[has_origin(role_changed => &role_changed.account)] + RoleRevoked(AccountRoleChanged), #[has_origin(metadata_changed => &metadata_changed.target)] MetadataInserted(AccountMetadataChanged), #[has_origin(metadata_changed => &metadata_changed.target)] @@ -389,13 +387,13 @@ mod domain { data_event! { #[has_origin(origin = Domain)] pub enum DomainEvent { - #[has_origin(account_event => &account_event.origin().domain)] - Account(AccountEvent), - #[has_origin(asset_definition_event => &asset_definition_event.origin().domain)] - AssetDefinition(AssetDefinitionEvent), #[has_origin(domain => domain.id())] Created(Domain), Deleted(DomainId), + #[has_origin(asset_definition_event => &asset_definition_event.origin().domain)] + AssetDefinition(AssetDefinitionEvent), + #[has_origin(account_event => &account_event.origin().domain)] + Account(AccountEvent), #[has_origin(metadata_changed => &metadata_changed.target)] MetadataInserted(DomainMetadataChanged), #[has_origin(metadata_changed => &metadata_changed.target)] @@ -488,14 +486,54 @@ mod trigger { } mod config { + pub use self::model::*; use super::*; + use crate::parameter::Parameter; - data_event! { - #[has_origin(origin = Parameter)] + #[model] + mod model { + use super::*; + + /// Changed parameter event + #[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Decode, + Encode, + Deserialize, + Serialize, + IntoSchema, + )] + #[ffi_type] + pub struct ParameterChanged { + /// Previous value for the parameter + pub prev: Parameter, + /// Next value for the parameter + pub next: Parameter, + } + + #[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + EventSet, + FromVariant, + Decode, + Encode, + Deserialize, + Serialize, + IntoSchema, + )] + #[ffi_type] pub enum ConfigurationEvent { - Changed(ParameterId), - Created(ParameterId), - Deleted(ParameterId), + Changed(ParameterChanged), } } } @@ -630,7 +668,7 @@ pub mod prelude { AssetDefinitionOwnerChanged, AssetDefinitionTotalQuantityChanged, AssetEvent, AssetEventSet, }, - config::{ConfigurationEvent, ConfigurationEventSet}, + config::{ConfigurationEvent, ConfigurationEventSet, ParameterChanged}, domain::{DomainEvent, DomainEventSet, DomainOwnerChanged}, executor::{ExecutorEvent, ExecutorEventSet, ExecutorUpgrade}, peer::{PeerEvent, PeerEventSet}, diff --git a/data_model/src/events/data/filters.rs b/data_model/src/events/data/filters.rs index 86bb1a886f3..0bda7058181 100644 --- a/data_model/src/events/data/filters.rs +++ b/data_model/src/events/data/filters.rs @@ -223,8 +223,6 @@ mod model { IntoSchema, )] pub struct ConfigurationEventFilter { - /// If specified matches only events originating from this configuration - pub(super) id_matcher: Option, /// Matches only event from this set pub(super) event_set: ConfigurationEventSet, } @@ -598,18 +596,10 @@ impl ConfigurationEventFilter { /// Creates a new [`ConfigurationEventFilter`] accepting all [`ConfigurationEvent`]s. pub const fn new() -> Self { Self { - id_matcher: None, event_set: ConfigurationEventSet::all(), } } - /// Modifies a [`ConfigurationEventFilter`] to accept only [`ConfigurationEvent`]s originating from ids matching `id_matcher`. - #[must_use] - pub fn for_parameter(mut self, id_matcher: ParameterId) -> Self { - self.id_matcher = Some(id_matcher); - self - } - /// Modifies a [`ConfigurationEventFilter`] to accept only [`ConfigurationEvent`]s of types matching `event_set`. #[must_use] pub const fn for_events(mut self, event_set: ConfigurationEventSet) -> Self { @@ -629,12 +619,6 @@ impl super::EventFilter for ConfigurationEventFilter { type Event = super::ConfigurationEvent; fn matches(&self, event: &Self::Event) -> bool { - if let Some(id_matcher) = &self.id_matcher { - if id_matcher != event.origin() { - return false; - } - } - if !self.event_set.matches(event) { return false; } diff --git a/data_model/src/events/pipeline.rs b/data_model/src/events/pipeline.rs index 731e194c505..0fa4f02f44c 100644 --- a/data_model/src/events/pipeline.rs +++ b/data_model/src/events/pipeline.rs @@ -2,6 +2,7 @@ #[cfg(not(feature = "std"))] use alloc::{boxed::Box, format, string::String, vec::Vec}; +use core::num::NonZeroU64; use iroha_crypto::HashOf; use iroha_data_model_derive::model; @@ -84,7 +85,7 @@ mod model { #[getset(get = "pub")] pub hash: HashOf, #[getset(get_copy = "pub")] - pub block_height: Option, + pub block_height: Option, #[getset(get = "pub")] pub status: TransactionStatus, } @@ -181,7 +182,7 @@ mod model { #[ffi_type] pub struct BlockEventFilter { #[getset(get_copy = "pub")] - pub height: Option, + pub height: Option, #[getset(get = "pub")] pub status: Option, } @@ -205,7 +206,7 @@ mod model { pub struct TransactionEventFilter { #[getset(get = "pub")] pub hash: Option>, - pub block_height: Option>, + pub block_height: Option>, #[getset(get = "pub")] pub status: Option, } @@ -223,7 +224,7 @@ impl BlockEventFilter { /// Match only block with the given height #[must_use] - pub fn for_height(mut self, height: u64) -> Self { + pub fn for_height(mut self, height: NonZeroU64) -> Self { self.height = Some(height); self } @@ -249,7 +250,7 @@ impl TransactionEventFilter { /// Match only transactions with the given block height #[must_use] - pub fn for_block_height(mut self, block_height: Option) -> Self { + pub fn for_block_height(mut self, block_height: Option) -> Self { self.block_height = Some(block_height); self } @@ -270,7 +271,7 @@ impl TransactionEventFilter { /// Block height // TODO: Derive with getset - pub fn block_height(&self) -> Option> { + pub fn block_height(&self) -> Option> { self.block_height } } @@ -345,12 +346,13 @@ mod tests { use alloc::{string::ToString as _, vec, vec::Vec}; use iroha_crypto::Hash; + use nonzero_ext::nonzero; use super::{super::EventFilter, *}; use crate::{transaction::error::TransactionRejectionReason::*, ValidationFail}; impl BlockHeader { - fn dummy(height: u64) -> Self { + fn dummy(height: NonZeroU64) -> Self { Self { height, prev_block_hash: None, @@ -375,7 +377,7 @@ mod tests { .into(), TransactionEvent { hash: HashOf::from_untyped_unchecked(Hash::prehashed([0_u8; Hash::LENGTH])), - block_height: Some(3), + block_height: Some(nonzero!(3_u64)), status: TransactionStatus::Rejected(Box::new(Validation( ValidationFail::TooComplex, ))), @@ -388,7 +390,7 @@ mod tests { } .into(), BlockEvent { - header: BlockHeader::dummy(7), + header: BlockHeader::dummy(nonzero!(7_u64)), hash: HashOf::from_untyped_unchecked(Hash::prehashed([7_u8; Hash::LENGTH])), status: BlockStatus::Committed, } @@ -418,7 +420,7 @@ mod tests { .into(), TransactionEvent { hash: HashOf::from_untyped_unchecked(Hash::prehashed([0_u8; Hash::LENGTH])), - block_height: Some(3), + block_height: Some(nonzero!(3_u64)), status: TransactionStatus::Rejected(Box::new(Validation( ValidationFail::TooComplex, ))), @@ -439,7 +441,7 @@ mod tests { vec![BlockEvent { status: BlockStatus::Committed, hash: HashOf::from_untyped_unchecked(Hash::prehashed([7_u8; Hash::LENGTH])), - header: BlockHeader::dummy(7), + header: BlockHeader::dummy(nonzero!(7_u64)), } .into()], ); diff --git a/data_model/src/executor.rs b/data_model/src/executor.rs index c0c264c749a..431cd08f81b 100644 --- a/data_model/src/executor.rs +++ b/data_model/src/executor.rs @@ -5,22 +5,22 @@ use alloc::{collections::BTreeSet, format, string::String, vec::Vec}; #[cfg(feature = "std")] use std::collections::BTreeSet; -use derive_more::{Constructor, Display}; -use getset::Getters; use iroha_data_model_derive::model; use iroha_primitives::json::JsonString; -use iroha_schema::IntoSchema; -use parity_scale_codec::{Decode, Encode}; -use serde::{Deserialize, Serialize}; pub use self::model::*; use crate::{permission::PermissionId, transaction::WasmSmartContract}; #[model] mod model { - use iroha_schema::Ident; + use derive_more::{Constructor, Display}; + use getset::Getters; + use iroha_schema::{Ident, IntoSchema}; + use parity_scale_codec::{Decode, Encode}; + use serde::{Deserialize, Serialize}; use super::*; + use crate::parameter::CustomParameters; /// executor that checks if an operation satisfies some conditions. /// @@ -80,19 +80,20 @@ mod model { #[ffi_type] #[display(fmt = "{self:?}")] pub struct ExecutorDataModel { + /// Corresponds to the id of [`InstructionBox::Custom`]. + /// + /// It should be set during executor migration, + /// so it can be retrieved through Iroha API. + pub custom_instruction: Option, + /// Corresponds to the [`Parameter::Custom`]. + /// Holds the initial value of the parameter + pub parameters: CustomParameters, /// Permission tokens supported by the executor. /// /// These IDs refer to the types in the schema. pub permissions: BTreeSet, - /// Type id in the schema. - /// Corresponds to payload of `InstructionBox::Custom`. - /// - /// Note that technically it is not needed - /// (custom instructions can be used without specifying it), - /// however it is recommended to set it, - /// so clients could retrieve it through Iroha API. - pub custom_instruction: Option, - /// Data model JSON schema, typically produced by [`IntoSchema`]. + /// Data model JSON schema. Includes description of all executor defined types + /// (permissions, parameters, instructions) pub schema: JsonString, } diff --git a/data_model/src/ipfs.rs b/data_model/src/ipfs.rs index 635900ba5c2..e6dbaca5c76 100644 --- a/data_model/src/ipfs.rs +++ b/data_model/src/ipfs.rs @@ -4,18 +4,19 @@ use alloc::{format, string::String, vec::Vec}; use core::str::FromStr; -use derive_more::Display; use iroha_data_model_derive::model; use iroha_primitives::conststr::ConstString; -use iroha_schema::IntoSchema; use parity_scale_codec::{Decode, Encode, Input}; -use serde_with::{DeserializeFromStr, SerializeDisplay}; pub use self::model::*; use crate::ParseError; #[model] mod model { + use derive_more::Display; + use iroha_schema::IntoSchema; + use serde_with::{DeserializeFromStr, SerializeDisplay}; + use super::*; /// Represents path in IPFS. Performs checks to ensure path validity. diff --git a/data_model/src/isi.rs b/data_model/src/isi.rs index 44d347cdd9c..1a2c22f8f8b 100644 --- a/data_model/src/isi.rs +++ b/data_model/src/isi.rs @@ -62,7 +62,7 @@ mod model { )] #[enum_ref(derive(Encode, FromVariant))] #[strum_discriminants( - name(InstructionType), + name(InstructionId), derive( Display, PartialOrd, @@ -114,8 +114,6 @@ mod model { #[debug(fmt = "{_0:?}")] SetParameter(SetParameter), #[debug(fmt = "{_0:?}")] - NewParameter(NewParameter), - #[debug(fmt = "{_0:?}")] Upgrade(Upgrade), #[debug(fmt = "{_0:?}")] Log(Log), @@ -177,7 +175,6 @@ impl_instruction! { Revoke, Revoke, SetParameter, - NewParameter, Upgrade, ExecuteTrigger, Log, @@ -253,31 +250,16 @@ mod transparent { }; } - isi! { - /// Generic instruction for setting a chain-wide config parameter. - #[derive(Constructor, Display)] - #[display(fmt = "SET `{parameter}`")] - #[serde(transparent)] - #[repr(transparent)] - pub struct SetParameter { - /// The configuration parameter being changed. - #[serde(flatten)] - pub parameter: Parameter, - } - } - - isi! { - /// Sized structure for all possible on-chain configuration parameters when they are first created. + iroha_data_model_derive::model_single! { /// Generic instruction for setting a chain-wide config parameter. - #[derive(Constructor, Display)] - #[display(fmt = "SET `{parameter}`")] + #[derive(Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord, Constructor)] + #[derive(parity_scale_codec::Decode, parity_scale_codec::Encode)] + #[derive(serde::Deserialize, serde::Serialize)] + #[derive(iroha_schema::IntoSchema)] + #[display(fmt = "SET `{_0}`")] #[serde(transparent)] #[repr(transparent)] - pub struct NewParameter { - /// Parameter to be changed. - #[serde(flatten)] - pub parameter: Parameter, - } + pub struct SetParameter(pub Parameter); } isi! { @@ -783,7 +765,7 @@ mod transparent { pub fn asset_store(asset_id: AssetId, to: AccountId) -> Self { Self { source: asset_id, - object: Metadata::new(), + object: Metadata::default(), destination: to, } } @@ -979,14 +961,16 @@ mod transparent { } isi! { - /// Custom instruction with arbitrary payload. - /// Should be handled in custom executor, where it will be translated to usual ISIs. + /// Blockchain specific instruction (defined in the executor). /// Can be used to extend instruction set or add expression system. - /// See `executor_custom_instructions_simple` and `executor_custom_instructions_complex` - /// examples in `client/tests/integration/smartcontracts`. /// - /// Note: If using custom instructions, it is recommended - /// to set `ExecutorDataModel::custom_instruction` in custom executor `migrate` entrypoint. + /// Note: If using custom instructions remember to set (during the executor migration) + /// [`ExecutorDataModel::custom_instruction`] + /// + /// # Examples + /// + /// Check `executor_custom_instructions_simple` and `executor_custom_instructions_complex` + /// integration tests #[derive(Display)] #[display(fmt = "CUSTOM({payload})")] pub struct CustomInstruction { @@ -1233,10 +1217,9 @@ pub mod error { use parity_scale_codec::{Decode, Encode}; pub use self::model::*; - use super::InstructionType; + use super::InstructionId; use crate::{ asset::AssetValueType, - metadata, query::error::{FindError, QueryExecutionFail}, IdBox, }; @@ -1286,8 +1269,6 @@ pub mod error { Mintability(#[cfg_attr(feature = "std", source)] MintabilityError), /// Illegal math operation Math(#[cfg_attr(feature = "std", source)] MathError), - /// Metadata error - Metadata(#[cfg_attr(feature = "std", source)] metadata::MetadataError), /// Invalid instruction parameter InvalidParameter(#[cfg_attr(feature = "std", source)] InvalidParameterError), /// Iroha invariant violation: {0} @@ -1321,7 +1302,7 @@ pub mod error { #[ffi_type(opaque)] pub enum InstructionEvaluationError { /// Unsupported parameter type for instruction of type `{0}` - Unsupported(InstructionType), + Unsupported(InstructionId), /// Failed to find parameter in a permission: {0} PermissionParameter(String), /// Incorrect value type @@ -1502,7 +1483,7 @@ pub mod error { #[ffi_type] pub struct RepetitionError { /// Instruction type - pub instruction_type: InstructionType, + pub instruction_type: InstructionId, /// Id of the object being repeated pub id: IdBox, } @@ -1519,7 +1500,7 @@ pub mod error { pub mod prelude { pub use super::{ AssetTransferBox, Burn, BurnBox, CustomInstruction, ExecuteTrigger, Grant, GrantBox, - InstructionBox, Log, Mint, MintBox, NewParameter, Register, RegisterBox, RemoveKeyValue, + InstructionBox, Log, Mint, MintBox, Register, RegisterBox, RemoveKeyValue, RemoveKeyValueBox, Revoke, RevokeBox, SetKeyValue, SetKeyValueBox, SetParameter, Transfer, TransferBox, Unregister, UnregisterBox, Upgrade, }; diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index 1ee131a53af..f17812ff3a4 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -10,19 +10,16 @@ extern crate alloc; #[cfg(not(feature = "std"))] use alloc::{boxed::Box, format, string::String, vec::Vec}; -use core::{fmt, fmt::Debug, ops::RangeInclusive, str::FromStr}; -use derive_more::{Constructor, Display, From, FromStr}; -use getset::Getters; +use derive_more::{Constructor, Display}; use iroha_crypto::PublicKey; -use iroha_data_model_derive::{model, EnumRef, IdEqOrdHash}; +use iroha_data_model_derive::{model, EnumRef}; use iroha_macro::FromVariant; use iroha_schema::IntoSchema; use iroha_version::{declare_versioned, version_with_scale}; use parity_scale_codec::{Decode, Encode}; use prelude::Executable; use serde::{Deserialize, Serialize}; -use serde_with::{DeserializeFromStr, SerializeDisplay}; use strum::FromRepr; pub use self::model::*; @@ -38,6 +35,7 @@ pub mod ipfs; pub mod isi; pub mod metadata; pub mod name; +pub mod parameter; pub mod peer; pub mod permission; pub mod query; @@ -112,7 +110,6 @@ mod seal { Revoke, SetParameter, - NewParameter, Upgrade, ExecuteTrigger, Log, @@ -182,8 +179,8 @@ pub struct EnumTryAsError { } // Manual implementation because this allow annotation does not affect `Display` derive -impl fmt::Display for EnumTryAsError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> fmt::Result { +impl core::fmt::Display for EnumTryAsError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!( f, "Expected: {}\nGot: {:?}", @@ -204,414 +201,16 @@ impl EnumTryAsError { } #[cfg(feature = "std")] -impl std::error::Error for EnumTryAsError {} - -pub mod parameter { - //! Structures, traits and impls related to `Paramater`s. - - use core::borrow::Borrow; - - use iroha_primitives::numeric::Numeric; - - pub use self::model::*; - use super::*; - use crate::isi::InstructionBox; - - /// Set of parameter names currently used by Iroha - #[allow(missing_docs)] - pub mod default { - pub const MAX_TRANSACTIONS_IN_BLOCK: &str = "MaxTransactionsInBlock"; - pub const BLOCK_TIME: &str = "BlockTime"; - pub const COMMIT_TIME_LIMIT: &str = "CommitTimeLimit"; - pub const TRANSACTION_LIMITS: &str = "TransactionLimits"; - pub const WSV_DOMAIN_METADATA_LIMITS: &str = "WSVDomainMetadataLimits"; - pub const WSV_ASSET_DEFINITION_METADATA_LIMITS: &str = "WSVAssetDefinitionMetadataLimits"; - pub const WSV_ACCOUNT_METADATA_LIMITS: &str = "WSVAccountMetadataLimits"; - pub const WSV_ASSET_METADATA_LIMITS: &str = "WSVAssetMetadataLimits"; - pub const WSV_TRIGGER_METADATA_LIMITS: &str = "WSVTriggerMetadataLimits"; - pub const WSV_IDENT_LENGTH_LIMITS: &str = "WSVIdentLengthLimits"; - pub const EXECUTOR_FUEL_LIMIT: &str = "ExecutorFuelLimit"; - pub const EXECUTOR_MAX_MEMORY: &str = "ExecutorMaxMemory"; - pub const WASM_FUEL_LIMIT: &str = "WASMFuelLimit"; - pub const WASM_MAX_MEMORY: &str = "WASMMaxMemory"; - } - - #[model] - mod model { - use super::*; - - #[derive( - Debug, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - FromVariant, - Decode, - Encode, - Deserialize, - Serialize, - IntoSchema, - )] - #[ffi_type(local)] - pub enum ParameterValueBox { - TransactionLimits(transaction::TransactionLimits), - MetadataLimits(metadata::Limits), - LengthLimits(LengthLimits), - Numeric( - #[skip_from] - #[skip_try_from] - Numeric, - ), - } - - /// Identification of a [`Parameter`]. - #[derive( - Debug, - Display, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Getters, - FromStr, - Decode, - Encode, - Deserialize, - Serialize, - IntoSchema, - )] - #[display(fmt = "{name}")] - #[getset(get = "pub")] - #[serde(transparent)] - #[repr(transparent)] - #[ffi_type(opaque)] - pub struct ParameterId { - /// [`Name`] unique to a [`Parameter`]. - pub name: Name, - } - - #[derive( - Debug, - Display, - Clone, - Constructor, - IdEqOrdHash, - Decode, - Encode, - DeserializeFromStr, - SerializeDisplay, - IntoSchema, - )] - #[display(fmt = "?{id}={val}")] - /// A chain-wide configuration parameter and its value. - #[ffi_type] - pub struct Parameter { - /// Unique [`Id`] of the [`Parameter`]. - pub id: ParameterId, - /// Current value of the [`Parameter`]. - pub val: ParameterValueBox, - } - } - - // TODO: Maybe derive - impl core::fmt::Display for ParameterValueBox { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::MetadataLimits(v) => core::fmt::Display::fmt(&v, f), - Self::TransactionLimits(v) => core::fmt::Display::fmt(&v, f), - Self::LengthLimits(v) => core::fmt::Display::fmt(&v, f), - Self::Numeric(v) => core::fmt::Display::fmt(&v, f), - } - } - } - - impl> From for ParameterValueBox { - fn from(value: T) -> Self { - Self::Numeric(value.into()) - } - } - - impl TryFrom for u32 { - type Error = iroha_macro::error::ErrorTryFromEnum; - - fn try_from(value: ParameterValueBox) -> Result { - use iroha_macro::error::ErrorTryFromEnum; - - let ParameterValueBox::Numeric(numeric) = value else { - return Err(ErrorTryFromEnum::default()); - }; - - numeric.try_into().map_err(|_| ErrorTryFromEnum::default()) - } - } - - impl TryFrom for u64 { - type Error = iroha_macro::error::ErrorTryFromEnum; - - fn try_from(value: ParameterValueBox) -> Result { - use iroha_macro::error::ErrorTryFromEnum; - - let ParameterValueBox::Numeric(numeric) = value else { - return Err(ErrorTryFromEnum::default()); - }; - - numeric.try_into().map_err(|_| ErrorTryFromEnum::default()) - } - } - - impl Parameter { - /// Current value of the [`Parameter`]. - pub fn val(&self) -> &ParameterValueBox { - &self.val - } - } - - impl Borrow for ParameterId { - fn borrow(&self) -> &str { - self.name.borrow() - } - } - - impl Borrow for Parameter { - fn borrow(&self) -> &str { - self.id.borrow() - } - } - - impl FromStr for Parameter { - type Err = ParseError; - - fn from_str(string: &str) -> Result { - if let Some((parameter_id_candidate, val_candidate)) = string.rsplit_once('=') { - if let Some(parameter_id_candidate) = parameter_id_candidate.strip_prefix('?') { - let param_id: ParameterId = - parameter_id_candidate.parse().map_err(|_| ParseError { - reason: "Failed to parse the `param_id` part of the `Parameter`.", - })?; - if let Some((val, ty)) = val_candidate.rsplit_once('_') { - let val = match ty { - // Shorthand for `LengthLimits` - "LL" => { - let (lower, upper) = val.rsplit_once(',').ok_or( ParseError { - reason: - "Failed to parse the `val` part of the `Parameter` as `LengthLimits`. Two comma-separated values are expected.", - })?; - let lower = lower.parse::().map_err(|_| ParseError { - reason: - "Failed to parse the `val` part of the `Parameter` as `LengthLimits`. Invalid lower `u32` bound.", - })?; - let upper = upper.parse::().map_err(|_| ParseError { - reason: - "Failed to parse the `val` part of the `Parameter` as `LengthLimits`. Invalid upper `u32` bound.", - })?; - LengthLimits::new(lower, upper).into() - } - // Shorthand for `TransactionLimits` - "TL" => { - let (max_instr, max_wasm_size) = val.rsplit_once(',').ok_or( ParseError { - reason: - "Failed to parse the `val` part of the `Parameter` as `TransactionLimits`. Two comma-separated values are expected.", - })?; - let max_instr = max_instr.parse::().map_err(|_| ParseError { - reason: - "Failed to parse the `val` part of the `Parameter` as `TransactionLimits`. `max_instruction_number` field should be a valid `u64`.", - })?; - let max_wasm_size = max_wasm_size.parse::().map_err(|_| ParseError { - reason: - "Failed to parse the `val` part of the `Parameter` as `TransactionLimits`. `max_wasm_size_bytes` field should be a valid `u64`.", - })?; - transaction::TransactionLimits::new( - max_instr, - max_wasm_size, - ).into() - } - // Shorthand for `MetadataLimits` - "ML" => { - let (lower, upper) = val.rsplit_once(',').ok_or( ParseError { - reason: - "Failed to parse the `val` part of the `Parameter` as `MetadataLimits`. Two comma-separated values are expected.", - })?; - let lower = lower.parse::().map_err(|_| ParseError { - reason: - "Failed to parse the `val` part of the `Parameter` as `MetadataLimits`. Invalid `u32` in `capacity` field.", - })?; - let upper = upper.parse::().map_err(|_| ParseError { - reason: - "Failed to parse the `val` part of the `Parameter` as `MetadataLimits`. Invalid `u32` in `max_entry_len` field.", - })?; - metadata::Limits::new(lower, upper).into() - } - _ => return Err(ParseError { - reason: - "Unsupported type provided for the `val` part of the `Parameter`.", - }), - }; - Ok(Self::new(param_id, val)) - } else { - let val = val_candidate.parse::().map_err(|_| ParseError { - reason: - "Failed to parse the `val` part of the `Parameter` as `Numeric`.", - })?; - - Ok(Self::new(param_id, val.into())) - } - } else { - Err(ParseError { - reason: "`param_id` part of `Parameter` must start with `?`", - }) - } - } else { - Err(ParseError { - reason: "The `Parameter` string did not contain the `=` character.", - }) - } - } - } - - /// Convenience tool for setting parameters - #[derive(Default)] - #[must_use] - pub struct ParametersBuilder { - parameters: Vec, - } - - /// Error associated with parameters builder - #[derive(From, Debug, Display, Copy, Clone)] - pub enum ParametersBuilderError { - /// Error emerged during parsing of parameter id - Parse(ParseError), - } - - #[cfg(feature = "std")] - impl std::error::Error for ParametersBuilderError {} - - impl ParametersBuilder { - /// Construct [`Self`] - pub fn new() -> Self { - Self::default() - } - - /// Add [`Parameter`] to self - /// - /// # Errors - /// - [`ParameterId`] parsing failed - pub fn add_parameter( - mut self, - parameter_id: &str, - val: impl Into, - ) -> Result { - let parameter = Parameter { - id: parameter_id.parse()?, - val: val.into(), - }; - self.parameters.push(parameter); - Ok(self) - } - - /// Create sequence isi for setting parameters - pub fn into_set_parameters(self) -> Vec { - self.parameters - .into_iter() - .map(isi::SetParameter::new) - .map(Into::into) - .collect() - } - - /// Create sequence isi for creating parameters - pub fn into_create_parameters(self) -> Vec { - self.parameters - .into_iter() - .map(isi::NewParameter::new) - .map(Into::into) - .collect() - } - } - - pub mod prelude { - //! Prelude: re-export of most commonly used traits, structs and macros in this crate. - - pub use super::{Parameter, ParameterId}; - } - - #[cfg(test)] - mod tests { - use super::*; - use crate::{ - prelude::{numeric, MetadataLimits}, - transaction::TransactionLimits, - }; - - const INVALID_PARAM: [&str; 4] = [ - "", - "Block?SyncGossipPeriod=20000", - "?BlockSyncGossipPeriod20000", - "?BlockSyncGossipPeriod=20000_u32", - ]; - - #[test] - fn test_invalid_parameter_str() { - assert!(matches!( - parameter::Parameter::from_str(INVALID_PARAM[0]), - Err(err) if err.reason == "The `Parameter` string did not contain the `=` character." - )); - assert!(matches!( - parameter::Parameter::from_str(INVALID_PARAM[1]), - Err(err) if err.reason == "`param_id` part of `Parameter` must start with `?`" - )); - assert!(matches!( - parameter::Parameter::from_str(INVALID_PARAM[2]), - Err(err) if err.to_string() == "The `Parameter` string did not contain the `=` character." - )); - assert!(matches!( - parameter::Parameter::from_str(INVALID_PARAM[3]), - Err(err) if err.to_string() == "Unsupported type provided for the `val` part of the `Parameter`." - )); - } - - #[test] - fn test_parameter_serialize_deserialize_consistent() { - let parameters = [ - Parameter::new( - ParameterId::from_str("TransactionLimits") - .expect("Failed to parse `ParameterId`"), - TransactionLimits::new(42, 24).into(), - ), - Parameter::new( - ParameterId::from_str("MetadataLimits").expect("Failed to parse `ParameterId`"), - MetadataLimits::new(42, 24).into(), - ), - Parameter::new( - ParameterId::from_str("LengthLimits").expect("Failed to parse `ParameterId`"), - LengthLimits::new(24, 42).into(), - ), - Parameter::new( - ParameterId::from_str("Int").expect("Failed to parse `ParameterId`"), - numeric!(42).into(), - ), - ]; - - for parameter in parameters { - assert_eq!( - parameter, - serde_json::to_string(¶meter) - .and_then(|parameter| serde_json::from_str(¶meter)) - .unwrap_or_else(|_| panic!( - "Failed to de/serialize parameter {:?}", - ¶meter - )) - ); - } - } - } +impl std::error::Error + for EnumTryAsError +{ } #[model] #[allow(clippy::redundant_pub_crate)] mod model { + use getset::Getters; + use super::*; /// Unique id of blockchain @@ -682,8 +281,8 @@ mod model { RoleId(role::RoleId), /// [`Permission`](`permission::Permission`) variant. PermissionId(permission::PermissionId), - /// [`ParameterId`](`parameter::ParameterId`) variant. - ParameterId(parameter::ParameterId), + /// [`CustomParameter`](`parameter::CustomParameter`) variant. + CustomParameterId(parameter::CustomParameterId), } /// Sized container for all possible entities. @@ -728,35 +327,8 @@ mod model { Trigger(trigger::Trigger), /// [`Role`](`role::Role`) variant. Role(role::Role), - /// [`Parameter`](`parameter::Parameter`) variant. - Parameter(parameter::Parameter), - } - - /// Limits of length of the identifiers (e.g. in [`domain::Domain`], [`account::Account`], [`asset::AssetDefinition`]) in number of chars - #[derive( - Debug, - Display, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Getters, - Decode, - Encode, - Deserialize, - Serialize, - IntoSchema, - )] - #[display(fmt = "{min},{max}_LL")] - #[getset(get = "pub")] - #[ffi_type] - pub struct LengthLimits { - /// Minimal length in number of chars (inclusive). - pub(super) min: u32, - /// Maximal length in number of chars (inclusive). - pub(super) max: u32, + /// [`CustomParameter`](`parameter::CustomParameter`) variant. + CustomParameter(parameter::CustomParameter), } /// Operation validation failed. @@ -906,7 +478,6 @@ impl_encode_as_id_box! { trigger::TriggerId, permission::PermissionId, role::RoleId, - parameter::ParameterId, } impl_encode_as_identifiable_box! { @@ -921,7 +492,6 @@ impl_encode_as_identifiable_box! { asset::Asset, trigger::Trigger, role::Role, - parameter::Parameter, } impl Decode for ChainId { @@ -960,7 +530,7 @@ impl IdentifiableBox { IdentifiableBox::Asset(a) => a.id().clone().into(), IdentifiableBox::Trigger(a) => a.id().clone().into(), IdentifiableBox::Role(a) => a.id().clone().into(), - IdentifiableBox::Parameter(a) => a.id().clone().into(), + IdentifiableBox::CustomParameter(a) => a.id().clone().into(), } } } @@ -1011,20 +581,6 @@ pub trait Registered: Identifiable { type With; } -impl LengthLimits { - /// Constructor. - pub const fn new(min: u32, max: u32) -> Self { - Self { min, max } - } -} - -impl From for RangeInclusive { - #[inline] - fn from(limits: LengthLimits) -> Self { - RangeInclusive::new(limits.min, limits.max) - } -} - declare_versioned!( BatchedResponse serde::Deserialize<'de>> 1..2, Debug, Clone, iroha_macro::FromVariant, IntoSchema @@ -1090,6 +646,6 @@ pub mod prelude { executor::prelude::*, isi::prelude::*, metadata::prelude::*, name::prelude::*, parameter::prelude::*, peer::prelude::*, permission::prelude::*, query::prelude::*, role::prelude::*, transaction::prelude::*, trigger::prelude::*, ChainId, EnumTryAsError, - HasMetadata, IdBox, Identifiable, IdentifiableBox, LengthLimits, ValidationFail, + HasMetadata, IdBox, Identifiable, IdentifiableBox, ValidationFail, }; } diff --git a/data_model/src/metadata.rs b/data_model/src/metadata.rs index 8c021e25fcc..8a5efe050aa 100644 --- a/data_model/src/metadata.rs +++ b/data_model/src/metadata.rs @@ -1,22 +1,13 @@ //! Metadata: key-value pairs that can be attached to accounts, transactions and assets. #[cfg(not(feature = "std"))] -use alloc::{ - collections::BTreeMap, - format, - string::{String, ToString}, - vec::Vec, -}; +use alloc::{collections::BTreeMap, format, string::String, vec::Vec}; use core::borrow::Borrow; #[cfg(feature = "std")] -use std::{collections::BTreeMap, string::ToString, vec::Vec}; +use std::{collections::BTreeMap, vec::Vec}; -use derive_more::Display; use iroha_data_model_derive::model; use iroha_primitives::json::JsonString; -use iroha_schema::IntoSchema; -use parity_scale_codec::{Decode, Encode}; -use serde::{Deserialize, Serialize}; pub use self::model::*; use crate::prelude::Name; @@ -25,11 +16,13 @@ use crate::prelude::Name; pub type Path = [Name]; -/// Collection of parameters by their names. -pub type UnlimitedMetadata = BTreeMap; - #[model] mod model { + use derive_more::Display; + use iroha_schema::IntoSchema; + use parity_scale_codec::{Decode, Encode}; + use serde::{Deserialize, Serialize}; + use super::*; /// Collection of parameters by their names with checked insertion. @@ -54,114 +47,9 @@ mod model { #[display(fmt = "Metadata")] #[allow(clippy::multiple_inherent_impl)] pub struct Metadata(pub(super) BTreeMap); - - /// Limits for [`Metadata`]. - #[derive( - Debug, - Display, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Decode, - Encode, - Deserialize, - Serialize, - IntoSchema, - )] - #[ffi_type] - #[display(fmt = "{capacity},{max_entry_len}_ML")] - pub struct Limits { - /// Maximum number of entries - pub capacity: u32, - /// Maximum length of entry - pub max_entry_len: u32, - } - - /// Metadata related errors. - #[derive( - Debug, - displaydoc::Display, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Decode, - Encode, - Deserialize, - Serialize, - IntoSchema, - )] - #[ffi_type(local)] - #[cfg_attr(feature = "std", derive(thiserror::Error))] - pub enum MetadataError { - /// Path specification empty - EmptyPath, - /// Metadata entry is too big - EntryTooBig(#[cfg_attr(feature = "std", source)] SizeError), - /// Metadata exceeds overall length limit - MaxCapacity(#[cfg_attr(feature = "std", source)] SizeError), - /// `{0}`: path segment not found, i.e. nothing was found at that key - MissingSegment(Name), - /// `{0}`: path segment not an instance of metadata - InvalidSegment(Name), - /// Metadata has an Invalid Json - InvalidJson(String), - } - - /// Size limits exhaustion error - #[derive( - Debug, - Display, - Copy, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Decode, - Encode, - Deserialize, - Serialize, - IntoSchema, - )] - #[ffi_type] - #[cfg_attr(feature = "std", derive(thiserror::Error))] - #[display(fmt = "Limits are {limits}, while the actual value is {actual}")] - pub struct SizeError { - /// The limits that were set for this entry - pub limits: Limits, - /// The actual *entry* size in bytes - pub actual: u64, - } -} - -impl Limits { - /// Constructor. - pub const fn new(capacity: u32, max_entry_len: u32) -> Limits { - Limits { - capacity, - max_entry_len, - } - } -} - -impl From for MetadataError { - fn from(err: serde_json::Error) -> Self { - MetadataError::InvalidJson(err.to_string()) - } } impl Metadata { - /// Constructor. - #[inline] - pub fn new() -> Self { - Self(UnlimitedMetadata::new()) - } - /// Check if the internal map contains the given key. pub fn contains(&self, key: &Name) -> bool { self.0.contains_key(key) @@ -181,37 +69,10 @@ impl Metadata { self.0.get(key) } - fn len_u64(&self) -> u64 { - self.0 - .len() - .try_into() - .expect("`usize` should always fit into `u64`") - } - /// Insert [`Value`] under the given key. Returns `Some(value)` /// if the value was already present, `None` otherwise. - /// - /// # Errors - /// Fails if `max_entry_len` or `capacity` from `limits` are exceeded. - pub fn insert_with_limits( - &mut self, - key: Name, - value: impl TryInto, - limits: Limits, - ) -> Result, MetadataError> { - let value = match value.try_into() { - Ok(value) => value, - _ => return Err(MetadataError::InvalidJson("Invalid Json value".to_string())), - }; - - if self.0.len() >= limits.capacity as usize && !self.0.contains_key(&key) { - return Err(MetadataError::MaxCapacity(SizeError { - limits, - actual: self.len_u64(), - })); - } - check_size_limits(&key, &value, limits)?; - Ok(self.0.insert(key, value)) + pub fn insert(&mut self, key: Name, value: impl Into) -> Option { + self.0.insert(key, value.into()) } } @@ -229,73 +90,7 @@ impl Metadata { } } -fn check_size_limits(key: &Name, value: &JsonString, limits: Limits) -> Result<(), MetadataError> { - let entry_bytes: Vec = (key, value).encode(); - let byte_size = entry_bytes.len(); - if byte_size > limits.max_entry_len as usize { - return Err(MetadataError::EntryTooBig(SizeError { - limits, - actual: byte_size - .try_into() - .expect("`usize` should always fit into `u64`"), - })); - } - Ok(()) -} - pub mod prelude { //! Prelude: re-export most commonly used traits, structs and macros from this module. - pub use super::{Limits as MetadataLimits, Metadata, UnlimitedMetadata}; -} - -#[cfg(test)] -mod tests { - #[cfg(not(feature = "std"))] - use alloc::{borrow::ToOwned as _, vec}; - use core::str::FromStr as _; - - use iroha_macro::FromVariant; - - use super::*; - use crate::ParseError; - - /// Error used in testing to make text more readable using the `?` operator. - #[derive(Debug, Display, Clone, FromVariant)] - pub enum TestError { - Parse(ParseError), - Metadata(MetadataError), - } - - #[test] - fn insert_exceeds_entry_size() -> Result<(), TestError> { - let mut metadata = Metadata::new(); - let limits = Limits::new(10, 5); - assert!(metadata - .insert_with_limits(Name::from_str("1")?, JsonString::new("2"), limits) - .is_ok()); - assert!(metadata - .insert_with_limits(Name::from_str("1")?, JsonString::new("23456"), limits) - .is_err()); - Ok(()) - } - - #[test] - // This test is a good candidate for both property-based and parameterised testing - fn insert_exceeds_len() -> Result<(), TestError> { - let mut metadata = Metadata::new(); - let limits = Limits::new(2, 5); - assert!(metadata - .insert_with_limits(Name::from_str("1")?, 0_u32, limits) - .is_ok()); - assert!(metadata - .insert_with_limits(Name::from_str("2")?, 0_u32, limits) - .is_ok()); - assert!(metadata - .insert_with_limits(Name::from_str("2")?, 1_u32, limits) - .is_ok()); - assert!(metadata - .insert_with_limits(Name::from_str("3")?, 0_u32, limits) - .is_err()); - Ok(()) - } + pub use super::Metadata; } diff --git a/data_model/src/name.rs b/data_model/src/name.rs index 6094cb7acf4..bf87b77a275 100644 --- a/data_model/src/name.rs +++ b/data_model/src/name.rs @@ -2,20 +2,21 @@ //! and related implementations and trait implementations. #[cfg(not(feature = "std"))] use alloc::{format, string::String, vec::Vec}; -use core::{borrow::Borrow, ops::RangeInclusive, str::FromStr}; +use core::{borrow::Borrow, str::FromStr}; -use derive_more::{DebugCustom, Display}; use iroha_data_model_derive::model; use iroha_primitives::conststr::ConstString; -use iroha_schema::IntoSchema; use parity_scale_codec::{Decode, Encode, Input}; use serde::{Deserialize, Serialize}; pub use self::model::*; -use crate::{isi::error::InvalidParameterError, ParseError}; +use crate::ParseError; #[model] mod model { + use derive_more::{DebugCustom, Display}; + use iroha_schema::IntoSchema; + use super::*; /// `Name` struct represents the type of Iroha Entities names, such as @@ -41,27 +42,6 @@ mod model { } impl Name { - /// Check if `range` contains the number of chars in the inner `ConstString` of this [`Name`]. - /// - /// # Errors - /// Fails if `range` does not - pub fn validate_len( - &self, - range: impl Into>, - ) -> Result<(), InvalidParameterError> { - let range = range.into(); - let Ok(true) = &self - .0 - .chars() - .count() - .try_into() - .map(|len| range.contains(&len)) - else { - return Err(InvalidParameterError::NameLength); - }; - Ok(()) - } - /// Check if `candidate` string would be valid [`Name`]. /// /// # Errors diff --git a/data_model/src/parameter.rs b/data_model/src/parameter.rs new file mode 100644 index 00000000000..4ad59afa219 --- /dev/null +++ b/data_model/src/parameter.rs @@ -0,0 +1,680 @@ +//! Structures, traits and impls related to `Paramater`s. +#[cfg(not(feature = "std"))] +use alloc::{collections::btree_map, format, string::String, vec::Vec}; +use core::{num::NonZeroU64, time::Duration}; +#[cfg(feature = "std")] +use std::collections::btree_map; + +use iroha_data_model_derive::model; +use iroha_primitives::json::JsonString; +use nonzero_ext::nonzero; + +pub use self::model::*; +use crate::name::Name; + +/// Collection of [`CustomParameter`]s +pub(crate) type CustomParameters = btree_map::BTreeMap; + +#[model] +mod model { + use derive_more::{Constructor, Display, FromStr}; + use getset::{CopyGetters, Getters}; + use iroha_data_model_derive::IdEqOrdHash; + use iroha_schema::IntoSchema; + use parity_scale_codec::{Decode, Encode}; + use serde::{Deserialize, Serialize}; + use strum::EnumDiscriminants; + + use super::*; + + /// Id of a custom parameter + #[derive( + Debug, + Display, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + FromStr, + Constructor, + Decode, + Encode, + Deserialize, + Serialize, + IntoSchema, + )] + #[ffi_type] + pub struct CustomParameterId(pub Name); + + /// Limits that govern consensus operation + #[derive( + Debug, + Display, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Decode, + Encode, + Deserialize, + Serialize, + IntoSchema, + )] + #[display(fmt = "{block_time_ms},{commit_time_ms}_SL")] + pub struct SumeragiParameters { + /// Maximal amount of time (in milliseconds) a peer will wait before forcing creation of a new block. + /// + /// A block is created if this limit or [`BlockParameters::max_transactions`] limit is reached, + /// whichever comes first. Regardless of the limits, an empty block is never created. + pub block_time_ms: u64, + /// Time (in milliseconds) a peer will wait for a block to be committed. + /// + /// If this period expires the block will request a view change + pub commit_time_ms: u64, + } + + /// Single Sumeragi parameter + /// + /// Check [`SumeragiParameters`] for more details + #[derive( + Debug, + Display, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Decode, + Encode, + Serialize, + Deserialize, + IntoSchema, + )] + pub enum SumeragiParameter { + BlockTimeMs(u64), + CommitTimeMs(u64), + } + + /// Limits that a block must obey to be accepted. + #[derive( + Debug, + Display, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + CopyGetters, + Encode, + Serialize, + IntoSchema, + )] + #[display(fmt = "{max_transactions}_BL")] + #[getset(get_copy = "pub")] + pub struct BlockParameters { + /// Maximal number of transactions in a block. + /// + /// A block is created if this limit is reached or [`SumeragiParameters::block_time_ms`] has expired, + /// whichever comes first. Regardless of the limits, an empty block is never created. + pub max_transactions: NonZeroU64, + } + + /// Single block parameter + /// + /// Check [`BlockParameters`] for more details + #[derive( + Debug, Display, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Serialize, IntoSchema, + )] + pub enum BlockParameter { + MaxTransactions(NonZeroU64), + } + + /// Limits that a transaction must obey to be accepted. + #[derive( + Debug, + Display, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + CopyGetters, + Encode, + Serialize, + IntoSchema, + )] + #[display(fmt = "{max_instructions},{smart_contract_size}_TL")] + #[getset(get_copy = "pub")] + pub struct TransactionParameters { + /// Maximum number of instructions per transaction + pub max_instructions: NonZeroU64, + /// Maximum size of wasm binary in bytes + pub smart_contract_size: NonZeroU64, + } + + /// Single transaction parameter + /// + /// Check [`TransactionParameters`] for more details + #[derive( + Debug, Display, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Serialize, IntoSchema, + )] + pub enum TransactionParameter { + MaxInstructions(NonZeroU64), + SmartContractSize(NonZeroU64), + } + + /// Limits that a smart contract must obey at runtime to considered valid. + #[derive( + Debug, + Display, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + CopyGetters, + Encode, + Serialize, + IntoSchema, + )] + #[display(fmt = "{fuel},{memory}_SCL")] + #[getset(get_copy = "pub")] + pub struct SmartContractParameters { + /// Maximum amount of fuel that a smart contract can consume + pub fuel: NonZeroU64, + /// Maximum amount of memory that a smart contract can use + pub memory: NonZeroU64, + } + + /// Single smart contract parameter + /// + /// Check [`SmartContractParameters`] for more details + #[derive( + Debug, Display, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Encode, Serialize, IntoSchema, + )] + pub enum SmartContractParameter { + Fuel(NonZeroU64), + Memory(NonZeroU64), + } + + /// Blockchain specific parameter defined in the executor + #[derive( + Debug, Display, Clone, IdEqOrdHash, Decode, Encode, Deserialize, Serialize, IntoSchema, + )] + #[ffi_type] + #[display(fmt = "{id}({payload})")] + pub struct CustomParameter { + /// Unique id of the parameter. + pub id: CustomParameterId, + /// Payload containing actual value. + /// + /// It is JSON-encoded, and its structure must correspond to the structure of + /// the type defined in [`crate::executor::ExecutorDataModel`]. + pub payload: JsonString, + } + + /// Set of all current blockchain parameter values + #[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Default, + Getters, + CopyGetters, + Decode, + Encode, + Deserialize, + Serialize, + IntoSchema, + )] + pub struct Parameters { + /// Sumeragi parameters + #[getset(get_copy = "pub")] + pub sumeragi: SumeragiParameters, + /// Block parameters + #[getset(get_copy = "pub")] + pub block: BlockParameters, + /// Transaction parameters + #[getset(get_copy = "pub")] + pub transaction: TransactionParameters, + /// Executor parameters + #[getset(get_copy = "pub")] + pub executor: SmartContractParameters, + /// Smart contract parameters + #[getset(get_copy = "pub")] + pub smart_contract: SmartContractParameters, + /// Collection of blockchain specific parameters + #[getset(get = "pub")] + pub custom: CustomParameters, + } + + /// Single blockchain parameter. + /// + /// Check [`Parameters`] for more details + #[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + EnumDiscriminants, + Decode, + Encode, + Deserialize, + Serialize, + IntoSchema, + )] + #[ffi_type(opaque)] + pub enum Parameter { + Sumeragi(SumeragiParameter), + Block(BlockParameter), + Transaction(TransactionParameter), + SmartContract(SmartContractParameter), + Executor(SmartContractParameter), + Custom(CustomParameter), + } +} + +impl core::fmt::Display for Parameter { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Sumeragi(v) => core::fmt::Display::fmt(&v, f), + Self::Block(v) => core::fmt::Display::fmt(&v, f), + Self::Transaction(v) => core::fmt::Display::fmt(&v, f), + Self::SmartContract(v) | Self::Executor(v) => core::fmt::Display::fmt(&v, f), + Self::Custom(v) => write!(f, "{}({})", v.id, v.payload), + } + } +} + +impl SumeragiParameters { + /// Maximal amount of time (in milliseconds) a peer will wait before forcing creation of a new block. + /// + /// A block is created if this limit or [`BlockParameters::max_transactions`] limit is reached, + /// whichever comes first. Regardless of the limits, an empty block is never created. + pub fn block_time(&self) -> Duration { + Duration::from_millis(self.block_time_ms) + } + + /// Time (in milliseconds) a peer will wait for a block to be committed. + /// + /// If this period expires the block will request a view change + pub fn commit_time(&self) -> Duration { + Duration::from_millis(self.commit_time_ms) + } + + /// Maximal amount of time it takes to commit a block + #[cfg(feature = "transparent_api")] + pub fn pipeline_time(&self) -> Duration { + self.block_time() + self.commit_time() + } + + /// Estimation of consensus duration + #[cfg(feature = "transparent_api")] + pub fn consensus_estimation(&self) -> Duration { + self.block_time() + (self.commit_time() / 2) + } +} + +impl Default for SumeragiParameters { + fn default() -> Self { + pub const DEFAULT_BLOCK_TIME: u64 = 2_000; + pub const DEFAULT_COMMIT_TIME: u64 = 4_000; + + Self { + block_time_ms: DEFAULT_BLOCK_TIME, + commit_time_ms: DEFAULT_COMMIT_TIME, + } + } +} +impl Default for BlockParameters { + fn default() -> Self { + /// Default value for [`Parameters::MaxTransactionsInBlock`] + pub const DEFAULT_TRANSACTIONS_IN_BLOCK: NonZeroU64 = nonzero!(2_u64.pow(9)); + + Self::new(DEFAULT_TRANSACTIONS_IN_BLOCK) + } +} + +impl Default for TransactionParameters { + fn default() -> Self { + const DEFAULT_INSTRUCTION_NUMBER: NonZeroU64 = nonzero!(2_u64.pow(12)); + const DEFAULT_SMART_CONTRACT_SIZE: NonZeroU64 = nonzero!(4 * 2_u64.pow(20)); + + Self::new(DEFAULT_INSTRUCTION_NUMBER, DEFAULT_SMART_CONTRACT_SIZE) + } +} + +impl Default for SmartContractParameters { + fn default() -> Self { + const DEFAULT_FUEL: NonZeroU64 = nonzero!(55_000_000_u64); + const DEFAULT_MEMORY: NonZeroU64 = nonzero!(55_000_000_u64); + + Self { + fuel: DEFAULT_FUEL, + memory: DEFAULT_MEMORY, + } + } +} + +impl SumeragiParameters { + /// Construct [`Self`] + pub fn new(block_time: Duration, commit_time: Duration) -> Self { + Self { + block_time_ms: block_time + .as_millis() + .try_into() + .expect("INTERNAL BUG: Time should fit into u64"), + commit_time_ms: commit_time + .as_millis() + .try_into() + .expect("INTERNAL BUG: Time should fit into u64"), + } + } +} + +impl BlockParameters { + /// Construct [`Self`] + pub const fn new(max_transactions: NonZeroU64) -> Self { + Self { max_transactions } + } +} + +impl TransactionParameters { + /// Construct [`Self`] + pub const fn new(max_instructions: NonZeroU64, smart_contract_size: NonZeroU64) -> Self { + Self { + max_instructions, + smart_contract_size, + } + } +} + +impl CustomParameterId { + /// Getter for name + pub fn name(&self) -> &Name { + &self.0 + } +} + +impl CustomParameter { + /// Constructor + pub fn new(id: CustomParameterId, payload: impl Into) -> Self { + Self { + id, + payload: payload.into(), + } + } + + /// Getter + // TODO: derive with getset once FFI impl is fixed + pub fn payload(&self) -> &JsonString { + &self.payload + } +} + +mod candidate { + use core::num::NonZeroUsize; + + use parity_scale_codec::{Decode, Input}; + use serde::Deserialize; + + use super::*; + + #[derive(Decode, Deserialize)] + enum TransactionParameterCandidate { + MaxInstructions(NonZeroU64), + SmartContractSize(NonZeroU64), + } + + #[derive(Decode, Deserialize)] + struct TransactionParametersCandidate { + max_instructions: NonZeroU64, + smart_contract_size: NonZeroU64, + } + + #[derive(Decode, Deserialize)] + enum BlockParameterCandidate { + MaxTransactions(NonZeroU64), + } + + #[derive(Decode, Deserialize)] + struct BlockParametersCandidate { + max_transactions: NonZeroU64, + } + + #[derive(Decode, Deserialize)] + enum SmartContractParameterCandidate { + Fuel(NonZeroU64), + Memory(NonZeroU64), + } + + #[derive(Decode, Deserialize)] + struct SmartContractParametersCandidate { + fuel: NonZeroU64, + memory: NonZeroU64, + } + + impl BlockParameterCandidate { + fn validate(self) -> Result { + Ok(match self { + Self::MaxTransactions(max_transactions) => { + let _ = NonZeroUsize::try_from(max_transactions) + .map_err(|_| "BlockParameter::MaxTransactions exceeds usize::MAX")?; + + BlockParameter::MaxTransactions(max_transactions) + } + }) + } + } + + impl BlockParametersCandidate { + fn validate(self) -> Result { + let _ = NonZeroUsize::try_from(self.max_transactions) + .map_err(|_| "BlockParameters::max_transactions exceeds usize::MAX")?; + + Ok(BlockParameters { + max_transactions: self.max_transactions, + }) + } + } + + impl TransactionParameterCandidate { + fn validate(self) -> Result { + Ok(match self { + Self::MaxInstructions(max_instructions) => { + let _ = NonZeroUsize::try_from(max_instructions) + .map_err(|_| "TransactionParameter::MaxInstructions exceeds usize::MAX")?; + TransactionParameter::MaxInstructions(max_instructions) + } + Self::SmartContractSize(smart_contract_size) => { + let _ = NonZeroUsize::try_from(smart_contract_size).map_err(|_| { + "TransactionParameter::SmartContractSize exceeds usize::MAX" + })?; + TransactionParameter::SmartContractSize(smart_contract_size) + } + }) + } + } + + impl TransactionParametersCandidate { + fn validate(self) -> Result { + let _ = NonZeroUsize::try_from(self.max_instructions) + .map_err(|_| "TransactionParameters::max_instructions exceeds usize::MAX")?; + + let _ = NonZeroUsize::try_from(self.smart_contract_size) + .map_err(|_| "TransactionParameters::smart_contract_size exceeds usize::MAX")?; + + Ok(TransactionParameters { + max_instructions: self.max_instructions, + smart_contract_size: self.smart_contract_size, + }) + } + } + + impl SmartContractParameterCandidate { + fn validate(self) -> Result { + Ok(match self { + Self::Fuel(fuel) => SmartContractParameter::Fuel(fuel), + Self::Memory(memory) => { + NonZeroUsize::try_from(memory) + .map_err(|_| "SmartContractParameter::Memory exceeds usize::MAX")?; + SmartContractParameter::Memory(memory) + } + }) + } + } + + impl SmartContractParametersCandidate { + fn validate(self) -> Result { + let _ = NonZeroUsize::try_from(self.memory) + .map_err(|_| "SmartContractParameters::memory exceeds usize::MAX")?; + + Ok(SmartContractParameters { + fuel: self.fuel, + memory: self.memory, + }) + } + } + + impl Decode for BlockParameter { + fn decode(input: &mut I) -> Result { + BlockParameterCandidate::decode(input)? + .validate() + .map_err(Into::into) + } + } + + impl<'de> Deserialize<'de> for BlockParameter { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + + BlockParameterCandidate::deserialize(deserializer)? + .validate() + .map_err(D::Error::custom) + } + } + + impl Decode for BlockParameters { + fn decode(input: &mut I) -> Result { + BlockParametersCandidate::decode(input)? + .validate() + .map_err(Into::into) + } + } + + impl<'de> Deserialize<'de> for BlockParameters { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + + BlockParametersCandidate::deserialize(deserializer)? + .validate() + .map_err(D::Error::custom) + } + } + + impl Decode for TransactionParameter { + fn decode(input: &mut I) -> Result { + TransactionParameterCandidate::decode(input)? + .validate() + .map_err(Into::into) + } + } + + impl<'de> Deserialize<'de> for TransactionParameter { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + + TransactionParameterCandidate::deserialize(deserializer)? + .validate() + .map_err(D::Error::custom) + } + } + + impl Decode for TransactionParameters { + fn decode(input: &mut I) -> Result { + TransactionParametersCandidate::decode(input)? + .validate() + .map_err(Into::into) + } + } + + impl<'de> Deserialize<'de> for TransactionParameters { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + + TransactionParametersCandidate::deserialize(deserializer)? + .validate() + .map_err(D::Error::custom) + } + } + + impl Decode for SmartContractParameter { + fn decode(input: &mut I) -> Result { + SmartContractParameterCandidate::decode(input)? + .validate() + .map_err(Into::into) + } + } + impl<'de> Deserialize<'de> for SmartContractParameter { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + + SmartContractParameterCandidate::deserialize(deserializer)? + .validate() + .map_err(D::Error::custom) + } + } + + impl Decode for SmartContractParameters { + fn decode(input: &mut I) -> Result { + SmartContractParametersCandidate::decode(input)? + .validate() + .map_err(Into::into) + } + } + impl<'de> Deserialize<'de> for SmartContractParameters { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + + SmartContractParametersCandidate::deserialize(deserializer)? + .validate() + .map_err(D::Error::custom) + } + } +} +pub mod prelude { + //! Prelude: re-export of most commonly used traits, structs and macros in this crate. + + pub use super::{Parameter, Parameters, SmartContractParameters, TransactionParameters}; +} diff --git a/data_model/src/peer.rs b/data_model/src/peer.rs index ffacb39dd98..72ab6b9a8e8 100644 --- a/data_model/src/peer.rs +++ b/data_model/src/peer.rs @@ -9,18 +9,19 @@ use core::{ }; use derive_more::Display; -use iroha_data_model_derive::{model, IdEqOrdHash}; +use iroha_data_model_derive::model; use iroha_primitives::addr::SocketAddr; -use iroha_schema::IntoSchema; -use parity_scale_codec::{Decode, Encode}; -use serde::{Deserialize, Serialize}; pub use self::model::*; -use crate::{Identifiable, PublicKey, Registered}; +use crate::{PublicKey, Registered}; #[model] mod model { use getset::Getters; + use iroha_data_model_derive::IdEqOrdHash; + use iroha_schema::IntoSchema; + use parity_scale_codec::{Decode, Encode}; + use serde::{Deserialize, Serialize}; use super::*; diff --git a/data_model/src/permission.rs b/data_model/src/permission.rs index 3c1ad8f6e0c..c6f1fceb59e 100644 --- a/data_model/src/permission.rs +++ b/data_model/src/permission.rs @@ -7,19 +7,20 @@ use std::collections::BTreeSet; use iroha_data_model_derive::model; use iroha_primitives::json::JsonString; use iroha_schema::IntoSchema; -use parity_scale_codec::{Decode, Encode}; -use serde::{Deserialize, Serialize}; pub use self::model::*; use crate::name::Name; -/// Collection of [`Token`]s +/// Collection of [`Permission`]s pub type Permissions = BTreeSet; -use super::*; - #[model] mod model { + use derive_more::{Constructor, Display, FromStr}; + use getset::Getters; + use parity_scale_codec::{Decode, Encode}; + use serde::{Deserialize, Serialize}; + use super::*; /// Identifies a [`Permission`]. @@ -68,7 +69,7 @@ mod model { Getters, )] #[ffi_type] - #[display(fmt = "PERMISSION `{id}` = `{payload}`")] + #[display(fmt = "{id}({payload})")] #[getset(get = "pub")] pub struct Permission { /// Refers to a type defined in [`crate::executor::ExecutorDataModel`]. diff --git a/data_model/src/query/cursor.rs b/data_model/src/query/cursor.rs index 3deaba04750..a9378501b21 100644 --- a/data_model/src/query/cursor.rs +++ b/data_model/src/query/cursor.rs @@ -16,7 +16,9 @@ use parity_scale_codec::{Decode, Encode, Input}; use serde::Serialize; pub use self::model::*; -use super::QueryId; + +/// Unique id of a query. +pub type QueryId = String; const QUERY_ID: &str = "query_id"; const CURSOR: &str = "cursor"; diff --git a/data_model/src/query/mod.rs b/data_model/src/query/mod.rs index fa1633bf0ea..71e7d3332f9 100644 --- a/data_model/src/query/mod.rs +++ b/data_model/src/query/mod.rs @@ -105,9 +105,6 @@ macro_rules! queries { }; } -/// Unique id of a query. -pub type QueryId = String; - /// Trait for typesafe query output pub trait Query: Into + seal::Sealed { /// Output type of query @@ -230,7 +227,8 @@ mod model { Identifiable(IdentifiableBox), Transaction(TransactionQueryOutput), Permission(crate::permission::Permission), - LimitedMetadata(JsonString), + Parameters(crate::parameter::Parameters), + Metadata(JsonString), Numeric(Numeric), BlockHeader(BlockHeader), Block(crate::block::SignedBlock), @@ -239,7 +237,7 @@ mod model { Vec( #[skip_from] #[skip_try_from] - Vec, + Vec, ), } @@ -420,7 +418,7 @@ impl_queries! { FindDomainById => crate::domain::Domain, FindDomainKeyValueByIdAndKey => JsonString, FindAllPeers => Vec, - FindAllParameters => Vec, + FindAllParameters => crate::parameter::Parameters, FindAllActiveTriggerIds => Vec, FindTriggerById => crate::trigger::Trigger, FindTriggerKeyValueByIdAndKey => JsonString, @@ -447,17 +445,18 @@ impl core::fmt::Display for QueryOutputBox { // TODO: Maybe derive fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - QueryOutputBox::Id(v) => core::fmt::Display::fmt(&v, f), - QueryOutputBox::Identifiable(v) => core::fmt::Display::fmt(&v, f), - QueryOutputBox::Transaction(_) => write!(f, "TransactionQueryOutput"), - QueryOutputBox::Permission(v) => core::fmt::Display::fmt(&v, f), - QueryOutputBox::Block(v) => core::fmt::Display::fmt(&v, f), - QueryOutputBox::BlockHeader(v) => core::fmt::Display::fmt(&v, f), - QueryOutputBox::Numeric(v) => core::fmt::Display::fmt(&v, f), - QueryOutputBox::LimitedMetadata(v) => core::fmt::Display::fmt(&v, f), - QueryOutputBox::ExecutorDataModel(v) => core::fmt::Display::fmt(&v, f), - - QueryOutputBox::Vec(v) => { + Self::Id(v) => core::fmt::Display::fmt(&v, f), + Self::Identifiable(v) => core::fmt::Display::fmt(&v, f), + Self::Transaction(_) => write!(f, "TransactionQueryOutput"), + Self::Permission(v) => core::fmt::Display::fmt(&v, f), + Self::Parameters(v) => core::fmt::Debug::fmt(&v, f), + Self::Block(v) => core::fmt::Display::fmt(&v, f), + Self::BlockHeader(v) => core::fmt::Display::fmt(&v, f), + Self::Numeric(v) => core::fmt::Display::fmt(&v, f), + Self::Metadata(v) => core::fmt::Display::fmt(&v, f), + Self::ExecutorDataModel(v) => core::fmt::Display::fmt(&v, f), + + Self::Vec(v) => { // TODO: Remove so we can derive. let list_of_display: Vec<_> = v.iter().map(ToString::to_string).collect(); // this prints with quotation marks, which is fine 90% @@ -486,7 +485,7 @@ macro_rules! from_and_try_from_value_idbox { impl From<$ty> for QueryOutputBox { fn from(id: $ty) -> Self { - QueryOutputBox::Id(IdBox::$variant(id)) + Self::Id(IdBox::$variant(id)) } })+ }; @@ -508,7 +507,7 @@ macro_rules! from_and_try_from_value_identifiable { impl From<$ty> for QueryOutputBox { fn from(id: $ty) -> Self { - QueryOutputBox::Identifiable(IdentifiableBox::$variant(id)) + Self::Identifiable(IdentifiableBox::$variant(id)) } } )+ }; @@ -522,7 +521,6 @@ from_and_try_from_value_idbox!( AssetDefinitionId(crate::asset::AssetDefinitionId), TriggerId(crate::trigger::TriggerId), RoleId(crate::role::RoleId), - ParameterId(crate::parameter::ParameterId), // TODO: Should we wrap String with new type in order to convert like here? //from_and_try_from_value_idbox!((DomainName(Name), ErrorValueTryFromDomainName),); ); @@ -539,12 +537,11 @@ from_and_try_from_value_identifiable!( Asset(crate::asset::Asset), Trigger(crate::trigger::Trigger), Role(crate::role::Role), - Parameter(crate::parameter::Parameter), ); impl> From> for QueryOutputBox { - fn from(values: Vec) -> QueryOutputBox { - QueryOutputBox::Vec(values.into_iter().map(Into::into).collect()) + fn from(values: Vec) -> Self { + Self::Vec(values.into_iter().map(Into::into).collect()) } } @@ -872,7 +869,7 @@ pub mod asset { } /// [`FindAssetQuantityById`] Iroha Query gets [`AssetId`] as input and finds [`Asset::quantity`] - /// parameter's value if [`Asset`] is presented in Iroha Peer. + /// value if [`Asset`] is presented in Iroha Peer. #[derive(Display)] #[display(fmt = "Find quantity of the `{id}` asset")] #[repr(transparent)] @@ -1556,8 +1553,6 @@ pub mod error { Role(RoleId), /// Failed to find [`Permission`] by id. Permission(PermissionId), - /// Parameter with id `{0}` not found - Parameter(ParameterId), /// Failed to find public key: `{0}` PublicKey(PublicKey), } @@ -1573,6 +1568,6 @@ pub mod prelude { account::prelude::*, asset::prelude::*, block::prelude::*, domain::prelude::*, executor::prelude::*, peer::prelude::*, permission::prelude::*, predicate::PredicateTrait, role::prelude::*, transaction::prelude::*, trigger::prelude::*, FetchSize, QueryBox, - QueryId, TransactionQueryOutput, + TransactionQueryOutput, }; } diff --git a/data_model/src/query/predicate.rs b/data_model/src/query/predicate.rs index 00a6dfcd6b1..a8c5b9defcf 100644 --- a/data_model/src/query/predicate.rs +++ b/data_model/src/query/predicate.rs @@ -603,7 +603,7 @@ pub mod string { IdBox::TriggerId(id) => self.applies(&id.to_string()), IdBox::RoleId(id) => self.applies(&id.to_string()), IdBox::PermissionId(id) => self.applies(&id.to_string()), - IdBox::ParameterId(id) => self.applies(&id.to_string()), + IdBox::CustomParameterId(id) => self.applies(&id.to_string()), } } } diff --git a/data_model/src/role.rs b/data_model/src/role.rs index 45c6b53732a..834ef75e57b 100644 --- a/data_model/src/role.rs +++ b/data_model/src/role.rs @@ -3,21 +3,23 @@ #[cfg(not(feature = "std"))] use alloc::{format, string::String, vec::Vec}; -use derive_more::{Constructor, Display, FromStr}; -use getset::Getters; -use iroha_data_model_derive::{model, IdEqOrdHash}; -use iroha_schema::IntoSchema; -use parity_scale_codec::{Decode, Encode}; -use serde::{Deserialize, Serialize}; +use iroha_data_model_derive::model; pub use self::model::*; use crate::{ permission::{Permission, Permissions}, - Identifiable, Name, Registered, + Name, Registered, }; #[model] mod model { + use derive_more::{Constructor, Display, FromStr}; + use getset::Getters; + use iroha_data_model_derive::IdEqOrdHash; + use iroha_schema::IntoSchema; + use parity_scale_codec::{Decode, Encode}; + use serde::{Deserialize, Serialize}; + use super::*; /// Identification of a role. diff --git a/data_model/src/transaction.rs b/data_model/src/transaction.rs index e5263a3452d..6da46f26437 100644 --- a/data_model/src/transaction.rs +++ b/data_model/src/transaction.rs @@ -23,13 +23,13 @@ pub use self::model::*; use crate::{ account::AccountId, isi::{Instruction, InstructionBox}, - metadata::UnlimitedMetadata, + metadata::Metadata, ChainId, }; #[model] mod model { - use getset::{CopyGetters, Getters}; + use getset::Getters; use super::*; use crate::account::AccountId; @@ -114,34 +114,7 @@ mod model { /// Random value to make different hashes for transactions which occur repeatedly and simultaneously. pub nonce: Option, /// Store for additional information. - pub metadata: UnlimitedMetadata, - } - - /// Container for limits that transactions must obey. - #[derive( - Debug, - Display, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - CopyGetters, - Decode, - Encode, - Deserialize, - Serialize, - IntoSchema, - )] - #[display(fmt = "{max_instruction_number},{max_wasm_size_bytes}_TL")] - #[getset(get_copy = "pub")] - #[ffi_type] - pub struct TransactionLimits { - /// Maximum number of instructions per transaction - pub max_instruction_number: u64, - /// Maximum size of wasm binary - pub max_wasm_size_bytes: u64, + pub metadata: Metadata, } /// Signature of transaction @@ -206,16 +179,6 @@ mod model { } } -impl TransactionLimits { - /// Construct [`Self`] - pub const fn new(max_instruction_number: u64, max_wasm_size_bytes: u64) -> Self { - Self { - max_instruction_number, - max_wasm_size_bytes, - } - } -} - impl FromIterator for Executable { fn from_iter>(iter: T) -> Self { Self::Instructions(iter.into_iter().map(Into::into).collect()) @@ -282,7 +245,7 @@ impl SignedTransaction { /// Return transaction metadata. #[inline] - pub fn metadata(&self) -> &UnlimitedMetadata { + pub fn metadata(&self) -> &Metadata { let SignedTransaction::V1(tx) = self; &tx.payload.metadata } @@ -596,7 +559,6 @@ pub mod error { Revoke(_) => "revoke", ExecuteTrigger(_) => "execute trigger", SetParameter(_) => "set parameter", - NewParameter(_) => "new parameter", Upgrade(_) => "upgrade", Log(_) => "log", Custom(_) => "custom", @@ -655,7 +617,7 @@ mod http { nonce: None, time_to_live_ms: None, instructions: Vec::::new().into(), - metadata: UnlimitedMetadata::new(), + metadata: Metadata::default(), }, } } @@ -722,7 +684,7 @@ mod http { } /// Adds metadata to the `Transaction` - pub fn with_metadata(mut self, metadata: UnlimitedMetadata) -> Self { + pub fn with_metadata(mut self, metadata: Metadata) -> Self { self.payload.metadata = metadata; self } diff --git a/data_model/src/trigger.rs b/data_model/src/trigger.rs index 1170a87c4e2..8be9660bc05 100644 --- a/data_model/src/trigger.rs +++ b/data_model/src/trigger.rs @@ -17,9 +17,7 @@ use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; pub use self::model::*; -use crate::{ - events::prelude::*, metadata::Metadata, transaction::Executable, Identifiable, Name, Registered, -}; +use crate::{events::prelude::*, metadata::Metadata, transaction::Executable, Name, Registered}; #[model] mod model { @@ -189,7 +187,7 @@ pub mod action { // TODO: At this point the authority is meaningless. authority, filter: filter.into(), - metadata: Metadata::new(), + metadata: Metadata::default(), } } diff --git a/data_model/src/visit.rs b/data_model/src/visit.rs index 0ca83ed6c38..cc0e92764f5 100644 --- a/data_model/src/visit.rs +++ b/data_model/src/visit.rs @@ -39,7 +39,6 @@ pub trait Visit { visit_upgrade(&Upgrade), visit_execute_trigger(&ExecuteTrigger), - visit_new_parameter(&NewParameter), visit_set_parameter(&SetParameter), visit_log(&Log), visit_custom(&CustomInstruction), @@ -232,9 +231,6 @@ pub fn visit_instruction( isi: &InstructionBox, ) { match isi { - InstructionBox::NewParameter(variant_value) => { - visitor.visit_new_parameter(authority, variant_value) - } InstructionBox::SetParameter(variant_value) => { visitor.visit_set_parameter(authority, variant_value) } @@ -426,7 +422,6 @@ leaf_visitors! { visit_mint_trigger_repetitions(&Mint), visit_burn_trigger_repetitions(&Burn), visit_upgrade(&Upgrade), - visit_new_parameter(&NewParameter), visit_set_parameter(&SetParameter), visit_execute_trigger(&ExecuteTrigger), visit_log(&Log), diff --git a/default_executor/src/lib.rs b/default_executor/src/lib.rs index a506d4df6dc..9e79fec18a9 100644 --- a/default_executor/src/lib.rs +++ b/default_executor/src/lib.rs @@ -49,7 +49,7 @@ impl Executor { /// If `migrate()` entrypoint fails then the whole `Upgrade` instruction /// will be denied and previous executor will stay unchanged. #[entrypoint] -pub fn migrate(block_height: u64) -> MigrationResult { +fn migrate(block_height: u64) -> MigrationResult { Executor::ensure_genesis(block_height)?; DataModelBuilder::with_default_permissions().build_and_set(); diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index 29601e7573c..a948666267c 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -17,59 +17,49 @@ }, "AccountEvent": { "Enum": [ - { - "tag": "Asset", - "discriminant": 0, - "type": "AssetEvent" - }, { "tag": "Created", - "discriminant": 1, + "discriminant": 0, "type": "Account" }, { "tag": "Deleted", - "discriminant": 2, - "type": "AccountId" - }, - { - "tag": "AuthenticationAdded", - "discriminant": 3, + "discriminant": 1, "type": "AccountId" }, { - "tag": "AuthenticationRemoved", - "discriminant": 4, - "type": "AccountId" + "tag": "Asset", + "discriminant": 2, + "type": "AssetEvent" }, { "tag": "PermissionAdded", - "discriminant": 5, + "discriminant": 3, "type": "AccountPermissionChanged" }, { "tag": "PermissionRemoved", - "discriminant": 6, + "discriminant": 4, "type": "AccountPermissionChanged" }, { - "tag": "RoleRevoked", - "discriminant": 7, + "tag": "RoleGranted", + "discriminant": 5, "type": "AccountRoleChanged" }, { - "tag": "RoleGranted", - "discriminant": 8, + "tag": "RoleRevoked", + "discriminant": 6, "type": "AccountRoleChanged" }, { "tag": "MetadataInserted", - "discriminant": 9, + "discriminant": 7, "type": "MetadataChanged" }, { "tag": "MetadataRemoved", - "discriminant": 10, + "discriminant": 8, "type": "MetadataChanged" } ] @@ -91,48 +81,40 @@ "repr": "u32", "masks": [ { - "name": "AnyAsset", + "name": "Created", "mask": 1 }, { - "name": "Created", + "name": "Deleted", "mask": 2 }, { - "name": "Deleted", + "name": "AnyAsset", "mask": 4 }, { - "name": "AuthenticationAdded", + "name": "PermissionAdded", "mask": 8 }, { - "name": "AuthenticationRemoved", + "name": "PermissionRemoved", "mask": 16 }, { - "name": "PermissionAdded", + "name": "RoleGranted", "mask": 32 }, - { - "name": "PermissionRemoved", - "mask": 64 - }, { "name": "RoleRevoked", - "mask": 128 - }, - { - "name": "RoleGranted", - "mask": 256 + "mask": 64 }, { "name": "MetadataInserted", - "mask": 512 + "mask": 128 }, { "name": "MetadataRemoved", - "mask": 1024 + "mask": 256 } ] } @@ -295,34 +277,34 @@ "type": "AssetDefinition" }, { - "tag": "MintabilityChanged", + "tag": "Delete", "discriminant": 1, "type": "AssetDefinitionId" }, { - "tag": "OwnerChanged", + "tag": "MetadataInserted", "discriminant": 2, - "type": "AssetDefinitionOwnerChanged" + "type": "MetadataChanged" }, { - "tag": "Deleted", + "tag": "MetadataRemoved", "discriminant": 3, - "type": "AssetDefinitionId" + "type": "MetadataChanged" }, { - "tag": "MetadataInserted", + "tag": "MintabilityChanged", "discriminant": 4, - "type": "MetadataChanged" + "type": "AssetDefinitionId" }, { - "tag": "MetadataRemoved", + "tag": "TotalQuantityChanged", "discriminant": 5, - "type": "MetadataChanged" + "type": "AssetDefinitionTotalQuantityChanged" }, { - "tag": "TotalQuantityChanged", + "tag": "OwnerChanged", "discriminant": 6, - "type": "AssetDefinitionTotalQuantityChanged" + "type": "AssetDefinitionOwnerChanged" } ] }, @@ -347,27 +329,27 @@ "mask": 1 }, { - "name": "MintabilityChanged", + "name": "Delete", "mask": 2 }, { - "name": "OwnerChanged", + "name": "MetadataInserted", "mask": 4 }, { - "name": "Deleted", + "name": "MetadataRemoved", "mask": 8 }, { - "name": "MetadataInserted", + "name": "MintabilityChanged", "mask": 16 }, { - "name": "MetadataRemoved", + "name": "TotalQuantityChanged", "mask": 32 }, { - "name": "TotalQuantityChanged", + "name": "OwnerChanged", "mask": 64 } ] @@ -592,7 +574,7 @@ "Struct": [ { "name": "height", - "type": "Option" + "type": "Option>" }, { "name": "status", @@ -604,7 +586,7 @@ "Struct": [ { "name": "height", - "type": "u64" + "type": "NonZero" }, { "name": "prev_block_hash", @@ -629,6 +611,23 @@ ] }, "BlockMessage": "SignedBlock", + "BlockParameter": { + "Enum": [ + { + "tag": "MaxTransactions", + "discriminant": 0, + "type": "NonZero" + } + ] + }, + "BlockParameters": { + "Struct": [ + { + "name": "max_transactions", + "type": "NonZero" + } + ] + }, "BlockPayload": { "Struct": [ { @@ -775,26 +774,12 @@ { "tag": "Changed", "discriminant": 0, - "type": "ParameterId" - }, - { - "tag": "Created", - "discriminant": 1, - "type": "ParameterId" - }, - { - "tag": "Deleted", - "discriminant": 2, - "type": "ParameterId" + "type": "ParameterChanged" } ] }, "ConfigurationEventFilter": { "Struct": [ - { - "name": "id_matcher", - "type": "Option" - }, { "name": "event_set", "type": "ConfigurationEventSet" @@ -808,14 +793,6 @@ { "name": "Changed", "mask": 1 - }, - { - "name": "Created", - "mask": 2 - }, - { - "name": "Deleted", - "mask": 4 } ] } @@ -847,6 +824,19 @@ } ] }, + "CustomParameter": { + "Struct": [ + { + "name": "id", + "type": "CustomParameterId" + }, + { + "name": "payload", + "type": "JsonString" + } + ] + }, + "CustomParameterId": "Name", "DataEvent": { "Enum": [ { @@ -965,24 +955,24 @@ "DomainEvent": { "Enum": [ { - "tag": "Account", + "tag": "Created", "discriminant": 0, - "type": "AccountEvent" + "type": "Domain" }, { - "tag": "AssetDefinition", + "tag": "Deleted", "discriminant": 1, - "type": "AssetDefinitionEvent" + "type": "DomainId" }, { - "tag": "Created", + "tag": "AssetDefinition", "discriminant": 2, - "type": "Domain" + "type": "AssetDefinitionEvent" }, { - "tag": "Deleted", + "tag": "Account", "discriminant": 3, - "type": "DomainId" + "type": "AccountEvent" }, { "tag": "MetadataInserted", @@ -1018,19 +1008,19 @@ "repr": "u32", "masks": [ { - "name": "AnyAccount", + "name": "Created", "mask": 1 }, { - "name": "AnyAssetDefinition", + "name": "Deleted", "mask": 2 }, { - "name": "Created", + "name": "AnyAssetDefinition", "mask": 4 }, { - "name": "Deleted", + "name": "AnyAccount", "mask": 8 }, { @@ -1203,14 +1193,18 @@ }, "ExecutorDataModel": { "Struct": [ - { - "name": "permissions", - "type": "SortedVec" - }, { "name": "custom_instruction", "type": "Option" }, + { + "name": "parameters", + "type": "SortedMap" + }, + { + "name": "permissions", + "type": "SortedVec" + }, { "name": "schema", "type": "JsonString" @@ -1486,14 +1480,9 @@ "discriminant": 10, "type": "PermissionId" }, - { - "tag": "Parameter", - "discriminant": 11, - "type": "ParameterId" - }, { "tag": "PublicKey", - "discriminant": 12, + "discriminant": 11, "type": "PublicKey" } ] @@ -1721,9 +1710,9 @@ "type": "PermissionId" }, { - "tag": "ParameterId", + "tag": "CustomParameterId", "discriminant": 8, - "type": "ParameterId" + "type": "CustomParameterId" } ] }, @@ -1785,9 +1774,9 @@ "type": "Role" }, { - "tag": "Parameter", + "tag": "CustomParameter", "discriminant": 11, - "type": "Parameter" + "type": "CustomParameter" } ] }, @@ -1848,24 +1837,19 @@ "discriminant": 10, "type": "SetParameter" }, - { - "tag": "NewParameter", - "discriminant": 11, - "type": "NewParameter" - }, { "tag": "Upgrade", - "discriminant": 12, + "discriminant": 11, "type": "Upgrade" }, { "tag": "Log", - "discriminant": 13, + "discriminant": 12, "type": "Log" }, { "tag": "Custom", - "discriminant": 14, + "discriminant": 13, "type": "CustomInstruction" } ] @@ -1875,7 +1859,7 @@ { "tag": "Unsupported", "discriminant": 0, - "type": "InstructionType" + "type": "InstructionId" }, { "tag": "PermissionParameter", @@ -1926,19 +1910,14 @@ "discriminant": 6, "type": "MathError" }, - { - "tag": "Metadata", - "discriminant": 7, - "type": "MetadataError" - }, { "tag": "InvalidParameter", - "discriminant": 8, + "discriminant": 7, "type": "InvalidParameterError" }, { "tag": "InvariantViolation", - "discriminant": 9, + "discriminant": 8, "type": "String" } ] @@ -1955,7 +1934,7 @@ } ] }, - "InstructionType": { + "InstructionId": { "Enum": [ { "tag": "Register", @@ -2001,21 +1980,17 @@ "tag": "SetParameter", "discriminant": 10 }, - { - "tag": "NewParameter", - "discriminant": 11 - }, { "tag": "Upgrade", - "discriminant": 12 + "discriminant": 11 }, { "tag": "Log", - "discriminant": 13 + "discriminant": 12 }, { "tag": "Custom", - "discriminant": 14 + "discriminant": 13 } ] }, @@ -2040,18 +2015,6 @@ "Ipv4Addr": "Array", "Ipv6Addr": "Array", "JsonString": "String", - "LengthLimits": { - "Struct": [ - { - "name": "min", - "type": "u32" - }, - { - "name": "max", - "type": "u32" - } - ] - }, "Level": { "Enum": [ { @@ -2076,18 +2039,6 @@ } ] }, - "Limits": { - "Struct": [ - { - "name": "capacity", - "type": "u32" - }, - { - "name": "max_entry_len", - "type": "u32" - } - ] - }, "Log": { "Struct": [ { @@ -2217,39 +2168,6 @@ } ] }, - "MetadataError": { - "Enum": [ - { - "tag": "EmptyPath", - "discriminant": 0 - }, - { - "tag": "EntryTooBig", - "discriminant": 1, - "type": "SizeError" - }, - { - "tag": "MaxCapacity", - "discriminant": 2, - "type": "SizeError" - }, - { - "tag": "MissingSegment", - "discriminant": 3, - "type": "Name" - }, - { - "tag": "InvalidSegment", - "discriminant": 4, - "type": "Name" - }, - { - "tag": "InvalidJson", - "discriminant": 5, - "type": "String" - } - ] - }, "Mint": { "Struct": [ { @@ -2381,14 +2299,6 @@ } ] }, - "NewParameter": { - "Struct": [ - { - "name": "parameter", - "type": "Parameter" - } - ] - }, "NewRole": { "Struct": [ { @@ -2456,11 +2366,8 @@ "Option>": { "Option": "NonZero" }, - "Option>": { - "Option": "Option" - }, - "Option": { - "Option": "ParameterId" + "Option>>": { + "Option": "Option>" }, "Option": { "Option": "PeerId" @@ -2489,9 +2396,6 @@ "Option": { "Option": "u32" }, - "Option": { - "Option": "u64" - }, "Pagination": { "Struct": [ { @@ -2505,46 +2409,76 @@ ] }, "Parameter": { - "Struct": [ + "Enum": [ { - "name": "id", - "type": "ParameterId" + "tag": "Sumeragi", + "discriminant": 0, + "type": "SumeragiParameter" }, { - "name": "val", - "type": "ParameterValueBox" + "tag": "Block", + "discriminant": 1, + "type": "BlockParameter" + }, + { + "tag": "Transaction", + "discriminant": 2, + "type": "TransactionParameter" + }, + { + "tag": "SmartContract", + "discriminant": 3, + "type": "SmartContractParameter" + }, + { + "tag": "Executor", + "discriminant": 4, + "type": "SmartContractParameter" + }, + { + "tag": "Custom", + "discriminant": 5, + "type": "CustomParameter" } ] }, - "ParameterId": { + "ParameterChanged": { "Struct": [ { - "name": "name", - "type": "Name" + "name": "prev", + "type": "Parameter" + }, + { + "name": "next", + "type": "Parameter" } ] }, - "ParameterValueBox": { - "Enum": [ + "Parameters": { + "Struct": [ { - "tag": "TransactionLimits", - "discriminant": 0, - "type": "TransactionLimits" + "name": "sumeragi", + "type": "SumeragiParameters" }, { - "tag": "MetadataLimits", - "discriminant": 1, - "type": "Limits" + "name": "block", + "type": "BlockParameters" }, { - "tag": "LengthLimits", - "discriminant": 2, - "type": "LengthLimits" + "name": "transaction", + "type": "TransactionParameters" }, { - "tag": "Numeric", - "discriminant": 3, - "type": "Numeric" + "name": "executor", + "type": "SmartContractParameters" + }, + { + "name": "smart_contract", + "type": "SmartContractParameters" + }, + { + "name": "custom", + "type": "SortedMap" } ] }, @@ -2922,33 +2856,38 @@ "type": "Permission" }, { - "tag": "LimitedMetadata", + "tag": "Parameters", "discriminant": 4, + "type": "Parameters" + }, + { + "tag": "Metadata", + "discriminant": 5, "type": "JsonString" }, { "tag": "Numeric", - "discriminant": 5, + "discriminant": 6, "type": "Numeric" }, { "tag": "BlockHeader", - "discriminant": 6, + "discriminant": 7, "type": "BlockHeader" }, { "tag": "Block", - "discriminant": 7, + "discriminant": 8, "type": "SignedBlock" }, { "tag": "ExecutorDataModel", - "discriminant": 8, + "discriminant": 9, "type": "ExecutorDataModel" }, { "tag": "Vec", - "discriminant": 9, + "discriminant": 10, "type": "Vec" } ] @@ -3208,7 +3147,7 @@ "Struct": [ { "name": "instruction_type", - "type": "InstructionType" + "type": "InstructionId" }, { "name": "id", @@ -3296,12 +3235,12 @@ "type": "RoleId" }, { - "tag": "PermissionRemoved", + "tag": "PermissionAdded", "discriminant": 2, "type": "RolePermissionChanged" }, { - "tag": "PermissionAdded", + "tag": "PermissionRemoved", "discriminant": 3, "type": "RolePermissionChanged" } @@ -3332,11 +3271,11 @@ "mask": 2 }, { - "name": "PermissionRemoved", + "name": "PermissionAdded", "mask": 4 }, { - "name": "PermissionAdded", + "name": "PermissionRemoved", "mask": 8 } ] @@ -3516,14 +3455,7 @@ } ] }, - "SetParameter": { - "Struct": [ - { - "name": "parameter", - "type": "Parameter" - } - ] - }, + "SetParameter": "Parameter", "Signature": { "Struct": [ { @@ -3598,15 +3530,29 @@ } ] }, - "SizeError": { + "SmartContractParameter": { + "Enum": [ + { + "tag": "Fuel", + "discriminant": 0, + "type": "NonZero" + }, + { + "tag": "Memory", + "discriminant": 1, + "type": "NonZero" + } + ] + }, + "SmartContractParameters": { "Struct": [ { - "name": "limits", - "type": "Limits" + "name": "fuel", + "type": "NonZero" }, { - "name": "actual", - "type": "u64" + "name": "memory", + "type": "NonZero" } ] }, @@ -3683,6 +3629,12 @@ "value": "Numeric" } }, + "SortedMap": { + "Map": { + "key": "CustomParameterId", + "value": "CustomParameter" + } + }, "SortedMap": { "Map": { "key": "Name", @@ -3728,6 +3680,32 @@ } ] }, + "SumeragiParameter": { + "Enum": [ + { + "tag": "BlockTimeMs", + "discriminant": 0, + "type": "u64" + }, + { + "tag": "CommitTimeMs", + "discriminant": 1, + "type": "u64" + } + ] + }, + "SumeragiParameters": { + "Struct": [ + { + "name": "block_time_ms", + "type": "u64" + }, + { + "name": "commit_time_ms", + "type": "u64" + } + ] + }, "TimeEvent": { "Struct": [ { @@ -3761,7 +3739,7 @@ }, { "name": "block_height", - "type": "Option" + "type": "Option>" }, { "name": "status", @@ -3777,7 +3755,7 @@ }, { "name": "block_height", - "type": "Option>" + "type": "Option>>" }, { "name": "status", @@ -3793,15 +3771,29 @@ } ] }, - "TransactionLimits": { + "TransactionParameter": { + "Enum": [ + { + "tag": "MaxInstructions", + "discriminant": 0, + "type": "NonZero" + }, + { + "tag": "SmartContractSize", + "discriminant": 1, + "type": "NonZero" + } + ] + }, + "TransactionParameters": { "Struct": [ { - "name": "max_instruction_number", - "type": "u64" + "name": "max_instructions", + "type": "NonZero" }, { - "name": "max_wasm_size_bytes", - "type": "u64" + "name": "smart_contract_size", + "type": "NonZero" } ] }, @@ -3833,7 +3825,7 @@ }, { "name": "metadata", - "type": "SortedMap" + "type": "Metadata" } ] }, diff --git a/ffi/src/std_impls.rs b/ffi/src/std_impls.rs index d45d2ece14e..ff65bbae07b 100644 --- a/ffi/src/std_impls.rs +++ b/ffi/src/std_impls.rs @@ -47,17 +47,25 @@ ffi_type! { niche_value=RefMutSlice::null_mut() } } +ffi_type! { + unsafe impl Transparent for core::ptr::NonNull { + type Target = *mut T; + + validation_fn=unsafe {|target: &*mut T| !target.is_null()}, + niche_value=core::ptr::null_mut() + } +} ffi_type! { unsafe impl Transparent for core::mem::ManuallyDrop { type Target = T; } } ffi_type! { - unsafe impl Transparent for core::ptr::NonNull { - type Target = *mut T; + unsafe impl Transparent for core::num::NonZeroU64 { + type Target = u64; - validation_fn=unsafe {|target: &*mut T| !target.is_null()}, - niche_value=core::ptr::null_mut() + validation_fn=unsafe {|target: &u64| *target != 0}, + niche_value=0 } } diff --git a/primitives/src/json.rs b/primitives/src/json.rs index 35968711752..f36be4d5f92 100644 --- a/primitives/src/json.rs +++ b/primitives/src/json.rs @@ -7,23 +7,22 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -use core::{ - fmt::{Display, Formatter}, - str::FromStr, -}; +use core::str::FromStr; #[cfg(feature = "std")] use std::{ string::{String, ToString}, vec::Vec, }; +use derive_more::Display; use iroha_schema::IntoSchema; use parity_scale_codec::{Decode, Encode}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; /// A valid `JsonString` that consists of valid String of Json type -#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq, IntoSchema, Encode, Decode)] +#[derive(Debug, Display, Clone, PartialOrd, PartialEq, Ord, Eq, IntoSchema, Encode, Decode)] +#[display(fmt = "{_0}")] pub struct JsonString(String); impl JsonString { @@ -154,12 +153,6 @@ impl AsRef for JsonString { } } -impl Display for JsonString { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", &self.0) - } -} - mod candidate { use super::*; diff --git a/schema/gen/src/lib.rs b/schema/gen/src/lib.rs index 0183c60fa13..6a4d727ac3b 100644 --- a/schema/gen/src/lib.rs +++ b/schema/gen/src/lib.rs @@ -203,13 +203,12 @@ types!( InstructionEvaluationError, InstructionExecutionError, InstructionExecutionFail, - InstructionType, + InstructionId, InvalidParameterError, IpfsPath, Ipv4Addr, Ipv6Addr, JsonString, - LengthLimits, Level, Log, MathError, @@ -220,8 +219,6 @@ types!( MetadataChanged, MetadataChanged, MetadataChanged, - MetadataError, - MetadataLimits, Mint, Mint, MintBox, @@ -232,7 +229,6 @@ types!( NewAccount, NewAssetDefinition, NewDomain, - NewParameter, NewRole, NonTrivial, NonZeroU32, @@ -254,7 +250,6 @@ types!( Option, Option, Option>, - Option, Option, Option, Option, @@ -265,8 +260,6 @@ types!( Option, Pagination, Parameter, - ParameterId, - ParameterValueBox, Peer, PeerEvent, PeerEventFilter, @@ -330,7 +323,6 @@ types!( SignedQueryV1, SignedTransaction, SignedTransactionV1, - SizeError, SocketAddr, SocketAddrHost, SocketAddrV4, @@ -346,7 +338,6 @@ types!( TransactionEvent, TransactionEventFilter, TransactionLimitError, - TransactionLimits, TransactionPayload, TransactionQueryOutput, TransactionRejectionReason, @@ -429,10 +420,8 @@ pub mod complete_data_model { InstructionEvaluationError, InstructionExecutionError, InvalidParameterError, MathError, MintabilityError, Mismatch, RepetitionError, TypeError, }, - InstructionType, + InstructionId, }, - metadata::{MetadataError, SizeError}, - parameter::ParameterValueBox, prelude::*, query::{ error::{FindError, QueryExecutionFail}, @@ -445,8 +434,8 @@ pub mod complete_data_model { ForwardCursor, Pagination, QueryOutputBox, Sorting, }, transaction::{ - error::TransactionLimitError, SignedTransactionV1, TransactionLimits, - TransactionPayload, TransactionSignature, + error::TransactionLimitError, SignedTransactionV1, TransactionPayload, + TransactionSignature, }, BatchedResponse, BatchedResponseV1, Level, }; diff --git a/smart_contract/executor/derive/src/default.rs b/smart_contract/executor/derive/src/default.rs index 04dc91b5fdd..a307de0674e 100644 --- a/smart_contract/executor/derive/src/default.rs +++ b/smart_contract/executor/derive/src/default.rs @@ -156,7 +156,6 @@ pub fn impl_derive_visit(emitter: &mut Emitter, input: &syn::DeriveInput) -> Tok "fn visit_burn_trigger_repetitions(operation: &Burn)", "fn visit_execute_trigger(operation: &ExecuteTrigger)", "fn visit_set_parameter(operation: &SetParameter)", - "fn visit_new_parameter(operation: &NewParameter)", "fn visit_upgrade(operation: &Upgrade)", "fn visit_log(operation: &Log)", "fn visit_custom(operation: &CustomInstruction)", diff --git a/smart_contract/executor/derive/src/lib.rs b/smart_contract/executor/derive/src/lib.rs index 4b11347a112..6c3360d5417 100644 --- a/smart_contract/executor/derive/src/lib.rs +++ b/smart_contract/executor/derive/src/lib.rs @@ -7,6 +7,7 @@ use proc_macro2::TokenStream; mod conversion; mod default; mod entrypoint; +mod parameter; mod permission; mod validate; @@ -100,6 +101,16 @@ pub fn derive_permission(input: TokenStream) -> Result { Ok(permission::impl_derive_permission(&input)) } +/// Derive macro for `Parameter` trait. +/// ``` +#[manyhow] +#[proc_macro_derive(Parameter)] +pub fn derive_parameter(input: TokenStream) -> Result { + let input = syn::parse2(input)?; + + Ok(parameter::impl_derive_parameter(&input)) +} + /// Derive macro for `ValidateGrantRevoke` trait. /// /// # Attributes diff --git a/smart_contract/executor/derive/src/parameter.rs b/smart_contract/executor/derive/src/parameter.rs new file mode 100644 index 00000000000..6064a9c840b --- /dev/null +++ b/smart_contract/executor/derive/src/parameter.rs @@ -0,0 +1,43 @@ +//! Module with [`derive_parameter`](crate::derive_parameter) macro implementation + +use proc_macro2::TokenStream; +use quote::quote; + +/// [`derive_parameter`](crate::derive_parameter()) macro implementation +pub fn impl_derive_parameter(input: &syn::DeriveInput) -> TokenStream { + let generics = &input.generics; + let ident = &input.ident; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + quote! { + impl #impl_generics ::iroha_executor::parameter::Parameter for #ident #ty_generics #where_clause {} + + impl #impl_generics TryFrom<&::iroha_executor::data_model::parameter::CustomParameter> for #ident #ty_generics #where_clause { + type Error = ::iroha_executor::TryFromDataModelObjectError; + + fn try_from(value: &::iroha_executor::data_model::parameter::CustomParameter) -> core::result::Result { + if *value.id() != ::id() { + return Err(Self::Error::Id(value.id().name().clone())); + } + + serde_json::from_str::(value.payload().as_ref()).map_err(Self::Error::Deserialize) + } + } + + impl #impl_generics From<#ident #ty_generics> for ::iroha_executor::data_model::parameter::CustomParameter #where_clause { + fn from(value: #ident #ty_generics) -> Self { + ::iroha_executor::data_model::parameter::CustomParameter::new( + <#ident as ::iroha_executor::parameter::Parameter>::id(), + ::serde_json::to_value::<#ident #ty_generics>(value) + .expect("INTERNAL BUG: Failed to serialize Executor data model entity"), + ) + } + } + + impl #impl_generics From<#ident #ty_generics> for ::iroha_executor::data_model::parameter::Parameter #where_clause { + fn from(value: #ident #ty_generics) -> Self { + Self::Custom(value.into()) + } + } + } +} diff --git a/smart_contract/executor/src/default.rs b/smart_contract/executor/src/default.rs index 9146b2fab66..e7102fba840 100644 --- a/smart_contract/executor/src/default.rs +++ b/smart_contract/executor/src/default.rs @@ -27,7 +27,7 @@ pub use domain::{ pub use executor::visit_upgrade; use iroha_smart_contract::data_model::isi::InstructionBox; pub use log::visit_log; -pub use parameter::{visit_new_parameter, visit_set_parameter}; +pub use parameter::visit_set_parameter; pub use peer::{visit_register_peer, visit_unregister_peer}; pub use permission::{visit_grant_account_permission, visit_revoke_account_permission}; use permissions::AnyPermission; @@ -85,9 +85,6 @@ pub fn visit_instruction( isi: &InstructionBox, ) { match isi { - InstructionBox::NewParameter(isi) => { - executor.visit_new_parameter(authority, isi); - } InstructionBox::SetParameter(isi) => { executor.visit_set_parameter(authority, isi); } @@ -1117,25 +1114,6 @@ pub mod asset { pub mod parameter { use super::*; - #[allow(clippy::needless_pass_by_value)] - pub fn visit_new_parameter( - executor: &mut V, - authority: &AccountId, - isi: &NewParameter, - ) { - if is_genesis(executor) { - execute!(executor, isi); - } - if permissions::parameter::CanCreateParameters.is_owned_by(authority) { - execute!(executor, isi); - } - - deny!( - executor, - "Can't create new configuration parameters outside genesis without permission" - ); - } - #[allow(clippy::needless_pass_by_value)] pub fn visit_set_parameter( executor: &mut V, diff --git a/smart_contract/executor/src/lib.rs b/smart_contract/executor/src/lib.rs index f2a430d5dc5..3f83df4dd37 100644 --- a/smart_contract/executor/src/lib.rs +++ b/smart_contract/executor/src/lib.rs @@ -19,6 +19,7 @@ use iroha_smart_contract_utils::{decode_with_length_prefix_from_raw, encode_and_ pub use smart_contract::{data_model, parse, stub_getrandom}; pub mod default; +pub mod parameter; pub mod permission; pub mod utils { @@ -188,8 +189,9 @@ pub enum TryFromDataModelObjectError { /// A convenience to build [`ExecutorDataModel`] from within the executor #[derive(Debug, Clone)] pub struct DataModelBuilder { - permissions: BTreeSet, custom_instruction: Option, + parameters: BTreeSet, + permissions: BTreeSet, schema: MetaMap, } @@ -199,8 +201,9 @@ impl DataModelBuilder { #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { - permissions: <_>::default(), custom_instruction: None, + parameters: <_>::default(), + permissions: <_>::default(), schema: <_>::default(), } } @@ -221,20 +224,31 @@ impl DataModelBuilder { builder } + /// Define a type of custom instruction in the data model. + /// Corresponds to payload of `InstructionBox::Custom`. + #[must_use] + pub fn with_custom_instruction(mut self) -> Self { + T::update_schema_map(&mut self.schema); + self.custom_instruction = Some(T::type_name()); + self + } + /// Define a permission in the data model #[must_use] - pub fn add_permission(mut self) -> Self { + pub fn add_parameter>( + mut self, + param: T, + ) -> Self { ::update_schema_map(&mut self.schema); - self.permissions.insert(::id()); + self.parameters.insert(param.into()); self } - /// Define a type of custom instruction in the data model. - /// Corresponds to payload of `InstructionBox::Custom`. + /// Define a permission in the data model #[must_use] - pub fn with_custom_instruction(mut self) -> Self { - T::update_schema_map(&mut self.schema); - self.custom_instruction = Some(T::type_name()); + pub fn add_permission(mut self) -> Self { + ::update_schema_map(&mut self.schema); + self.permissions.insert(::id()); self } @@ -281,8 +295,12 @@ impl DataModelBuilder { } set_data_model(&ExecutorDataModel::new( - self.permissions, self.custom_instruction, + self.parameters + .into_iter() + .map(|param| (param.id().clone(), param)) + .collect(), + self.permissions, serde_json::to_value(&self.schema) .expect("INTERNAL BUG: Failed to serialize Executor data model entity") .into(), @@ -308,8 +326,8 @@ pub mod prelude { pub use alloc::vec::Vec; pub use iroha_executor_derive::{ - entrypoint, Constructor, Permission, Validate, ValidateEntrypoints, ValidateGrantRevoke, - Visit, + entrypoint, Constructor, Parameter, Permission, Validate, ValidateEntrypoints, + ValidateGrantRevoke, Visit, }; pub use iroha_smart_contract::prelude::*; @@ -320,6 +338,7 @@ pub mod prelude { ValidationFail, }, deny, execute, + parameter::Parameter as ParameterTrait, permission::Permission as PermissionTrait, DataModelBuilder, Validate, }; diff --git a/smart_contract/executor/src/parameter.rs b/smart_contract/executor/src/parameter.rs new file mode 100644 index 00000000000..22a61c74a3d --- /dev/null +++ b/smart_contract/executor/src/parameter.rs @@ -0,0 +1,17 @@ +//! Module with parameter related functionality. + +use iroha_schema::IntoSchema; +use iroha_smart_contract::{data_model::parameter::CustomParameterId, debug::DebugExpectExt}; +use serde::{de::DeserializeOwned, Serialize}; + +/// Blockchain specific parameter +pub trait Parameter: Default + Serialize + DeserializeOwned + IntoSchema { + /// Parameter id, according to [`IntoSchema`]. + fn id() -> CustomParameterId { + CustomParameterId::new( + ::type_name() + .parse() + .dbg_expect("Failed to parse parameter id as `Name`"), + ) + } +} diff --git a/smart_contract/executor/src/permission.rs b/smart_contract/executor/src/permission.rs index 6aa5c7d2a13..42874c6285a 100644 --- a/smart_contract/executor/src/permission.rs +++ b/smart_contract/executor/src/permission.rs @@ -1,4 +1,4 @@ -//! Module with permission tokens and permission related functionality. +//! Module with permission related functionality. use alloc::borrow::ToOwned as _; @@ -9,7 +9,7 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::prelude::{Permission as PermissionObject, *}; -/// Is used to check if the permission token is owned by the account. +/// Used to check if the permission token is owned by the account. pub trait Permission: Serialize + DeserializeOwned + IntoSchema + PartialEq + ValidateGrantRevoke { @@ -117,7 +117,7 @@ pub mod asset_definition { /// Check if `authority` is the owner of asset definition - /// `authority` is owner of asset_definition if: + /// `authority` is owner of asset definition if: /// - `asset_definition.owned_by` is `authority` /// - `asset_definition.domain_id` domain is owned by `authority` /// diff --git a/tools/kagami/src/genesis/generate.rs b/tools/kagami/src/genesis/generate.rs index bd52996c8be..5c1e165aa02 100644 --- a/tools/kagami/src/genesis/generate.rs +++ b/tools/kagami/src/genesis/generate.rs @@ -5,12 +5,7 @@ use std::{ use clap::{Parser, Subcommand}; use color_eyre::eyre::WrapErr as _; -use iroha_config::parameters::defaults::chain_wide as chain_wide_defaults; -use iroha_data_model::{ - metadata::Limits, - parameter::{default::*, ParametersBuilder}, - prelude::*, -}; +use iroha_data_model::prelude::*; use iroha_genesis::{GenesisBuilder, RawGenesisTransaction, GENESIS_DOMAIN_ID}; use serde_json::json; use test_samples::{gen_account_in, ALICE_ID, BOB_ID, CARPENTER_ID}; @@ -92,12 +87,8 @@ pub fn generate_default( genesis_public_key: PublicKey, ) -> color_eyre::Result { let genesis_account_id = AccountId::new(GENESIS_DOMAIN_ID.clone(), genesis_public_key); - let mut meta = Metadata::new(); - meta.insert_with_limits( - "key".parse()?, - JsonString::new("value"), - Limits::new(1024, 1024), - )?; + let mut meta = Metadata::default(); + meta.insert("key".parse()?, JsonString::new("value")); let mut builder = builder .domain_with_metadata("wonderland".parse()?, meta.clone()) @@ -138,7 +129,7 @@ pub fn generate_default( "wonderland".parse()?, ALICE_ID.clone(), ); - let register_user_metadata_access = Register::role( + let register_user_metadata_access: InstructionBox = Register::role( Role::new("ALICE_METADATA_ACCESS".parse()?) .add_permission(Permission::new( "CanSetKeyValueInAccount".parse()?, @@ -151,62 +142,6 @@ pub fn generate_default( ) .into(); - let parameter_defaults = ParametersBuilder::new() - .add_parameter( - MAX_TRANSACTIONS_IN_BLOCK, - Numeric::new(chain_wide_defaults::MAX_TXS.get().into(), 0), - )? - .add_parameter( - BLOCK_TIME, - Numeric::new(chain_wide_defaults::BLOCK_TIME.as_millis(), 0), - )? - .add_parameter( - COMMIT_TIME_LIMIT, - Numeric::new(chain_wide_defaults::COMMIT_TIME.as_millis(), 0), - )? - .add_parameter(TRANSACTION_LIMITS, chain_wide_defaults::TRANSACTION_LIMITS)? - .add_parameter( - WSV_DOMAIN_METADATA_LIMITS, - chain_wide_defaults::METADATA_LIMITS, - )? - .add_parameter( - WSV_ASSET_DEFINITION_METADATA_LIMITS, - chain_wide_defaults::METADATA_LIMITS, - )? - .add_parameter( - WSV_ACCOUNT_METADATA_LIMITS, - chain_wide_defaults::METADATA_LIMITS, - )? - .add_parameter( - WSV_ASSET_METADATA_LIMITS, - chain_wide_defaults::METADATA_LIMITS, - )? - .add_parameter( - WSV_TRIGGER_METADATA_LIMITS, - chain_wide_defaults::METADATA_LIMITS, - )? - .add_parameter( - WSV_IDENT_LENGTH_LIMITS, - chain_wide_defaults::IDENT_LENGTH_LIMITS, - )? - .add_parameter( - EXECUTOR_FUEL_LIMIT, - Numeric::new(chain_wide_defaults::WASM_FUEL_LIMIT.into(), 0), - )? - .add_parameter( - EXECUTOR_MAX_MEMORY, - Numeric::new(chain_wide_defaults::WASM_MAX_MEMORY.get().into(), 0), - )? - .add_parameter( - WASM_FUEL_LIMIT, - Numeric::new(chain_wide_defaults::WASM_FUEL_LIMIT.into(), 0), - )? - .add_parameter( - WASM_MAX_MEMORY, - Numeric::new(chain_wide_defaults::WASM_MAX_MEMORY.get().into(), 0), - )? - .into_create_parameters(); - for isi in [ mint.into(), mint_cabbage.into(), @@ -215,7 +150,6 @@ pub fn generate_default( grant_permission_to_set_parameters.into(), ] .into_iter() - .chain(parameter_defaults.into_iter()) .chain(std::iter::once(register_user_metadata_access)) { builder = builder.append_instruction(isi); diff --git a/tools/parity_scale_cli/samples/trigger.bin b/tools/parity_scale_cli/samples/trigger.bin index c6493efbb4fc7f5944c8326a4d307ccc98907299..d46095a94fdceb5b1a2fcb144965c9d41520524e 100644 GIT binary patch delta 12 RcmZo>Y-XHL$I8I~1OOA70we$c delta 12 RcmZo>Y-XHL$I8e61OO9v0v!MV diff --git a/tools/parity_scale_cli/src/main.rs b/tools/parity_scale_cli/src/main.rs index 6657ff2f59d..bd162df8b47 100644 --- a/tools/parity_scale_cli/src/main.rs +++ b/tools/parity_scale_cli/src/main.rs @@ -313,13 +313,11 @@ mod tests { #[test] fn decode_account_sample() { - let limits = MetadataLimits::new(256, 256); - let mut metadata = Metadata::new(); + let mut metadata = Metadata::default(); metadata - .insert_with_limits( + .insert( "hat".parse().expect("Valid"), "white".parse::().expect("Valid"), - limits, ) .expect("Valid"); let account = Account::new(ALICE_ID.clone()).with_metadata(metadata); @@ -329,10 +327,9 @@ mod tests { #[test] fn decode_domain_sample() { - let limits = MetadataLimits::new(256, 256); - let mut metadata = Metadata::new(); + let mut metadata = Metadata::default(); metadata - .insert_with_limits("Is_Jabberwocky_alive".parse().expect("Valid"), true, limits) + .insert("Is_Jabberwocky_alive".parse().expect("Valid"), true) .expect("Valid"); let domain = Domain::new("wonderland".parse().expect("Valid")) .with_logo( diff --git a/torii/src/routing.rs b/torii/src/routing.rs index 87c2381673e..3f8dbe997ef 100644 --- a/torii/src/routing.rs +++ b/torii/src/routing.rs @@ -52,7 +52,7 @@ pub async fn handle_transaction( transaction: SignedTransaction, ) -> Result { let state_view = state.view(); - let transaction_limits = state_view.config.transaction_limits; + let transaction_limits = state_view.world().parameters().transaction; let transaction = AcceptedTransaction::accept(transaction, &chain_id, transaction_limits) .map_err(Error::AcceptTransaction)?; queue