From c03fd9a66b7040d35446b0bf328c30997a882320 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 | 6 +- .../src/lib.rs | 5 +- .../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 509363 -> 518033 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/queue.rs | 58 +- core/src/smartcontracts/isi/account.rs | 11 +- core/src/smartcontracts/isi/asset.rs | 9 +- core/src/smartcontracts/isi/domain.rs | 17 +- core/src/smartcontracts/isi/mod.rs | 3 +- core/src/smartcontracts/isi/query.rs | 43 +- core/src/smartcontracts/isi/triggers/mod.rs | 11 +- .../isi/triggers/specialized.rs | 2 +- core/src/smartcontracts/isi/world.rs | 111 +-- core/src/smartcontracts/wasm.rs | 44 +- 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 | 16 +- 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 | 30 +- data_model/src/ipfs.rs | 7 +- data_model/src/isi.rs | 53 +- 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 | 7 +- data_model/src/query/mod.rs | 46 +- 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 | 502 +++++++------ ffi/src/std_impls.rs | 16 +- primitives/src/json.rs | 15 +- schema/gen/src/lib.rs | 15 +- 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 +- 112 files changed, 2012 insertions(+), 2490 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 85aecc3e6df..a1f8a9d2115 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 8fea7736954..d623bef784d 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..a9ae532107f 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,9 +77,11 @@ 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::() + .add_instruction::() + .add_instruction::() + .add_instruction::() .build_and_set(); Ok(()) 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..f1dfafe37d1 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,9 +55,10 @@ 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::() + .add_instruction::() + .add_instruction::() .build_and_set(); Ok(()) 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 7d4b5a03e4f..bae12a3ed30 100644 --- a/client/tests/integration/sorting.rs +++ b/client/tests/integration/sorting.rs @@ -52,14 +52,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), @@ -147,14 +141,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()); @@ -208,14 +196,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); @@ -256,14 +238,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); @@ -303,14 +279,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); @@ -377,14 +347,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 1aef328fdef..242bf039412 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 cb6308378dd..6ef4c4f9a6f 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_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_type -): - with allure.step( - f'WHEN client_cli registers the asset_definition "{GIVEN_129_length_name}" ' - f'with "{GIVEN_numeric_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, - type_=GIVEN_numeric_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_type") def test_register_asset_definition_with_store_type( GIVEN_fake_asset_name, GIVEN_registered_domain, GIVEN_store_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 81dd1dd3b7b..3ccf2ddea90 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 83ca0b9350b3b40adaa473f7e2eb873233c35746..8cf4a4e0d8c6bd6044d5e6315334ab63ebdb42ad 100644 GIT binary patch delta 201997 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

Ho|hP=*1La9)VPl%QtB~0 zjf_662D{HDs@=iKcjr(V{B+};^dvl3zEM`dda(!*Cl_Jd^&k$@{fu46YnP8NxYrdl6Wie*8;bo z@Vz@~m)!LGf+v*P?%iqWq3+#9$;7tx_ePv;>)l6+Dl~pxDLO!JU++#xUB%QNQ+qO1 zQG9=sU+&(>*hr*aJ1VVfrbQxYZ8b%kP+#(kBSk4yRFsaSm0c{EP6UODmL|0A4k{Uu z*{oXXQfcjU;}8X{oM{I@Pm^Q6u9FoQ%ms)z;Sb$Yi}%r=w2Q z{>4$`ELJV`(RAENwlwO|E9gWLe0n%4VhGQoaocY0%4ceZfrhgaI{1 z+Z1?rI))b0(Z*;5i40P9OFCgvNL00?EkB{t1(WqkwBjt%E%izK0y*`z-49P2RZBW) z_s7%d$$AOlzf|B~jK{|4Mm2k8F;TccfJ>pn2942S2bf%iZc=p#_7-VL+XlJlwLG10H-^@~N>+b;o98I&HqE|CZPsZW`L})^{OxCNAmqwXRled<68~Jt%Mo;8p^mbJ` zVpkKgEqXS8-q!ZOND*GHix5Rtc92UM+Sn zC>y07byt@SFKs#jw506O1iHzocDI&Q517zW&q>-A$&!vUwZ$FMukz>y7CRACD%3rz zSf#DBavEA>?$a%(^mZgWr35xcnh_EOHN`_%&?iobRuRdK(FPQ7&+j+>i&Y$BBn@a& zTs1;&b~?g#w380njgS^KGJSH4;1$xg|DH&1Q!T?HMV*ksZdv~knYps^uz-eP>T2=6 z6Lf==K~#gD%Y}eui7Ze|_8qH9;rgikW6&WfV+H6i4I0{n1&N$Zpn;hEaKi3T>6rZp ziE>k!58#0Vqcs<%Lx{tUH8*;O!Kn~<`ccK>BMJ|(&;%H`>- ziniE4aZAd7G|EgYpvU|G?a~aUK%UU2g>@>Kn8$~2sB#}JpI|vvb(Z_L^7N79b(SHA zWQ+Z(GJ~5MKv2gO0K%MLSkY8xz%1}CJvK7KDZxAor!oBck)5?B;Q+T_(PvwB!Ic?~n6||s1`}MKhq%GDQNHA&bNX2B* zJX=L8v8aAgd19LL6lo^m8)SiigDMds*&SUqNZ+E|u|sQ$z_1BLEZF13{Mo<17j)ZP1o|qkD7J0fnnosS#s$x^Zxzl{dN#)qAO5 zyWgtbWB7HswlQT`rsepV)MDR&W}K!I>>D8hpL5%*2W8Nx)1Ry$>ckGIanuPVP;6mQ zVCGQN!n#Re2%9~?MyUwcmK)eomJ~7XxA8p~G~eIszb8S2npmixL!! z<<2Va+$r(&f%mdZab7p9AvpL|LKkM#X+tAG`U&-V7*K+N)e&J0B94=~DayG6&I{cK z2cC$bHBhW_jy0v~;rKB^bv8y@W%6GFM@W*VK%xS_+Sx1_0aHN+xFP;_fG7GKjeP1T z!@-)SMCaovDYd_fVoRVc_=2Vo#jfQPg8BZjW@IsB)?~dH8OU1ame!6vxKWRxs?ngQ zQrPifMtzY3AR6?f`gqM`4Pil%3qF#LPv-h`AdY4*r zG0sMJW$n{KLeCX4j~wW#(<^)pDi+Xg$lU5teP{i zI)o)SXR=&_{(*bSpo8Ku2Rtq2-ZN;*v_g9*=(C9MDg&=F@TzobN^~kR!G9>)0Pz^} zVcXU=*b)7_8Hxo zH5E28Ys+WWR)|>>rDWF$vg?_|Z;oAuR(^MOZB{;|Z8{7nGkG550c zQ5o`sc$Nj^aqPHZB>*AXpliY0@G}f}cB<=Q92tZ&hD)&pN)IE1%+#k=L5RvCgzO-e z<@B$p)GT4ZABWYWVu;nhT(>t^y|*eFJ=?vu{vdUax6q1CamNiExX1Mh6Kc3vQBj33 zs0bEZ#0CqB05W@@qN3IBDqWS)T)XSYYleBJyl4$_+lKZVEW*CNwz>F6^)*+5qCovM z%_WBmBPuCz?;o1ZB=8EE@h`K$2_TP6a3q;6n zi?-BjdpITv_+1n=cy5c@5qku47ElKo$){~#3A4~v#BKl@!Bj+A4J^uJ`>D?FhnkT6 zN&2ep(pNPmolrxeCzqtCw;R(LffBF@8%ws-VI*TCi=s1;jyfGwwd_%NYLB(l7obKz zJY&%50x09q>9<9pd6%y-@2zeO~H?Rs?3}*qvk#MR`#M~@FE93)<`3n|#k^kFfk#B1b zrmcWu7x1B%qOZ1sdUc#JjH1pe0BfPO`)-Di7-f$^L5vxqek?0D==K<-_32_^)yD-p zje1S2u7-;V18+dW9!C^QL-zP|99nENU1LV-bhvjPIUrNeV(-Q>Bpb`#9c!?fh>w!- zE&#-2?%PRS8-Ggcj6-a;rU>D&T|h&F02Ji(7p0QC47kWKGoTz~i+8DkISK}4NslU% z*7hXAYN#=w5MPH#fkFbJ52}Dx@@k-j5OBgL;HofDr30cu4+}`|U1=(_3q%F-`q)I( z8%@5$SQq`A(8D#t-Uf=rOK~DDhpL%qj^+TS>)DzzrR}GKHoS1dRIVO zAx5<^0S;kLHTdn7Q3W(brqq>D(GjvEMitN$7*2olm*s`ag1f+sJJx6c=Gso2wxLUkNu14a&`RpDEPW7Fwg{Y?j4U&=4yqb|CxQY8I23H>}y;A+}F5P!ACi(xW7*g!OZptizMFnLo<4E#-L?MceKDD zT1)d@s6C6CH8iq1knbx}fg}fAiL!_J5j9YNgBnPFtfoaU$^_Pq!?&CVI2d zJdv^%5$IOen=s0Kd_vh?4km~2s=(=n*NwVW)k8B8G^2@YSp{Uff=!``<@_z2te3i_ z6KnS18Y^JG*GBD`{M{C%mdHWCzQphLsDW=qv`*Qx>d0MN-8mBnf?@y9#Bo&|y^}hm zctD!AH_W=?6Vi$OiwC&v6DwiDK;MHmYPvelXFkxvvdW^f5q_HDbg9v40FRo@0_gr_iFnvH&?J%icd-6xf|Qjm6W5My6=@h~0*(PWOkqRUfvf z5Y=Pj)gsMBdl6HL(3SwTg;X@jKJ<9NS>MkoI;rSeJyo4YaIv zU)}8ha9ucZF#4)=f74#-gmjEe(SMPm@=7oGi7*Ga36NGkB03W-^+m=tPEPD>LOPW$ z4$BsD=8YJyfYjgg(Ju|}MZa`^+BzS76g_2sU0mg^+?^h<2X-Hs5v>@^ppDuwP_Pl^ zB{9Y4swq)Xp5YN#XDDBot1}F4vP6wpxLguW+NnZDTA3`24;B)+5(Mx;cv@NrlL+o} zV@3+_E09%!tj`6uHF~sZWJVVQ%_Jx|Cb;&0Fbi&cWVBg827WK{h3#UCR3%nI!c5*` z_x4GHkLWCcNGnR0qT4Mcf#E?IH)T^Yw$2iNoN$7Rr2wMOvlKAn#7TpmKeM>P9k)lt zXbc{w1+;@R{@*eg5meTl>Z5Ra}K~d|jn^6Awoh?5u zyX>&gyNiK(eqLUBHV z6}gdZv_7*7)@~2Hmu-T1MBtsDZIlz5OT8JE;C{|xa39b@ps!_wpup35w=BukU|9wV zTX%ge*gWXLu!LCKv0<6Xvn;c!+KT?P9)%{1ZMmX<&I|zdV0c<#(~`zq!akIMu8IeS zDH)M&TwJTAy z_Jt_ExI?1IWT^tJXQ`skM6uzM5=AD}%M>4;BZ?lTI3NmYtwKj2icpQRE<{n*D^X;6 z@j-D(bYT^r2~lLY>nJj1r>vr9PO-6H7f!LUUmrNdmja^57#~z`tOBj?I#Ddaen}T+ z6B&MwoFdbc-XBR}756m7N92g2hbazf(5eZDB2)ugwBiK_cnh>??PS0zVlZ$+5`^rP zj8oZ1vb!F<1ATL2QrggsNyV+afM7w zl@@8Zm~;Qp^iN{<>X^NUjHAk^X$S)nk#zOYzgwx#!8(D?pzUUkWMjf<#D2JY=iVbS zbiv>~OaRf54ZlGNWA_*nAFK1rD8uPz8~d_7;8ir=khP?t`RaA?@N(IPZhhMYOXe$r zMmW*;X8;XR(vUIx9wd3LN^W<5HHYkYDwabP6*IH^1Qb(_3vmBpi{-KaBDO-d5zru- z`%PXAS8fpFfJCn>?&n7@3I7W^A#ASBiva~iQL?e67( z#IR9-);nT(B-S1iUtf){nlELL1WFkmleY zns($Nx0U7>3BmUn0r>8`=8JG}h5T1QL$GFO;@RbtXFkNvFri#bKlaQ3O($G2V)tj4 zZb#DK^@(QS6;0tieW)~V{iRDwy`8u)m^4e5B32fzZ;0!qQF4&*Qr(0J;8Nuz`sm+8 z^wU#IEC+N%ZX!#S92f1{b93tz+f#XD!GW>JJku`bO~y)}E@ufN6bI5KafvMeMu~6w zn27H+7=)?k`-lEDnY;}d|FvR>D)WVMV(K|aRY7_aDaaX$0Y|4|C;W~aYb3e+Q}(Pp9aUJ zV$_}EuJn~7!*OM)t`y~{JI8G%{iHKf;J^{42&M`~h-Z;>GKV6;jSeMz%}DUhL?FST zfaHaT?3`B%mcqO`Lw?+6W-Y=okVL3RL9h$FnzC^s*>a9oyEhy(pd8z5oV`5W?Ec}P zsTs5n0cIjiQ0HTtW^^0u)HLgEnGGPI-5i0RqG`6tHX0J<*dEQ!Her)5yg}QeX?E;q zXqo|{zM5ti^&Dfe3#J(lI{2z=A!dk_&du0wPq$a>(5w3cE!4QS!U7b%j+@9MJReC;Hl zMdrVU$h*%fpk);ZH5`Grr(RW7&u9^xlkkbFrwF_gU=<0W*8gJyS^#E_Vs{+Svg*tp zDRvhET3|H?Je0E-(9(%HiiP(kkGsYF8swn?LnIFkRUv`qQEZP+`@AYWIqkbstg?IM zp;Vb-_DL86b+z8M^u&7h?V+v|%kl4oVu_W2U_T8WiXKL^3u)_g>qnBB>Jib z@n(LyB@p!*<*8b=1C0JMjZMS^@)5_oH-IRN{!Cvs^F`#FVABzXaE4B zEvhz>A-*p<22Dne^%DC5e?A!u82i{kKwxK%oHG#6y#{EZ{?hLI$x!N`r1n)w3_Azu zIts}76hqF~J)4Cy1NrnEa`1BdnN>*6eiaGg=}OLY-};INRl_d5uuAvs2D(-#8g#bx$X0hjH4PCJEu?@#{!fmr zM!S$3a~w@f%z4_X8|LR8KuH2CT1wS)g*B)qh=i(b35v=4gi zcRxcC21I=(;f0@+gnO~B{wqhqJF>3Ax@@RQUv=3~l^%3iI>WKa(#6Qi5+(H7Z|{^U zcV=w`x-IqCUDOoHK1-XwqbpT*^o1(F_YbnR09|hs+SS@xz37utXfM{*_j42)wzj_2 z#o7w%uAwS@)m=kX@^n}C`D3aOdHTWypXjJSAl5&oWkn1u`3D{q*aD&qTUL5YXC z{NNvKSpm!(b@r}_avQ1h$30T#E~uitSXTd?qt4zet2wZ&!YV+hN?%ogP?hc~08g`I z#dx_9<2z!QRPN(DHLdy-zmj#5e&%8vbC_>hE$-?pU2Nx!|K3fj|M?d-tuUm3o_A(i zZCvn4={Xd45jL&<*Ub;foT;Egb7vYArJ=ofcwff%d#}> ztENZpR0-IbYWgom)4It&OWNL*mfQQ{`2XQ&sG0(zfD~ar?=U8-?ff1|@snFuKgzMz z9a&dlQVdn;D=CJm^gxP%btNVh7^giSQ3Bva#=0t&BRRCc%2O$^uH1Fs9+!cW1G|gd zry?7M3x;9YSRVA{jE$@|=Y1s>2m657pM-m&F|B$vyATbO7pC9V-4-zZ8knVYtz@_Y zLq~dh4o+c^MAxNBN6Be9tapu=pkWzQp+*y!!J#TZj_DunCUYnqR%cL$q1HXrNp&%u zfyt055JqP2h@`OqMA8FpW%`WCMQ3y)-8@Y84Ut5X?>?fHu?<1>;&KvyxCv}0;_~<+ z@#NPbQzBMCF?Og5@g`OLpePbpP=%xxATm_Nui@X};NHs_}sQj`Z_agxYx zHHQGj_9vm7B)O;ixm}5Lad!d5O$dRzyZR-E?c|ax524$-i4*73RdZB247Us*;>B#xZr-=W1_+4XW+4{7J!WC}yJz z$1d|Xx;B-_Mwgt-{(o`eQ&{H$!f1tHlP?IJtPRvK?y+rKy2Ck;*JV8teGtCpa5sf7B}Z?Lz@9l)dEhS=pO$78iFBzKe@_>vU`R)4rsAbSJQiau3A8gQttnrE0p^+5cexgg!tmFhPY_d$>@`YZ)0 zKnjR#AJapCkrhHiW$8?x;4GBPD( zv$@muVlB(X99H%UhK+?FAOB(wcyh-F>%DjPI}o+;%x?+1#UO6`EZGx4%4bjF7s6R) z(D43(ayoG{R3&hwncUQ~vinv8i8@SO0de(m-X7+pSuhu1Zk=_Aj-?=3%bN-1L^IVd zBi1Y%q|~4@Yh(VUMevPS_Js<6T-JrFqgx$q4`Q8;P!(-oq_WOjY`gPtwH(kpJ$pb; zB3m&C|DxUev(OUv4CO8FC{+-&6o_RWDyHH|r%7iCJWHhhi^UbLWIYpJXkHYaV0v&I zjbaYfl5iMryF35P3i$j?V?5W3!}Z{oynr9SLMG<)ZM!n3_T+5%MmCEnlKZn3Nn`_$ zDD!St-nAts#*!fh5KK|eKK2X{d5iu=(?!) zQOg)$^d#ke-|nLpG-Mrb84v`Hi{Sb+-efHP7^wHg{}05-b<#$==O;zz(kEX0TLc#G zDqiB<@RDilW%B{(0A4H{5aw4q3|=l&=72Lmkb}bj$>0Taj7>n|#pio+9M0bZGY5-% zV13^P^=9P2Mo+wNXEAL#!z5o&?nE`?iV20Z#j&{Hx$Rj}X063Ocm;Knzn#6JEALy; z7vATA_sM3~XMKbS;Pu1@%hM_oW`tor*t)2jl-)%>2rt^s`Cvx$d!G@0ntTwHl*b3t z{#D>+5LrH$ffNTV^mHkN*=q|z;{ra2{gm!}aPhA5L7yYI-#@doJ3DOSh4}um*M=Ro zAzqn>m_Gw{2oHac?2s&9&ZLqZ0;GT)y7Srw<61_uRRS*1N3Pdrrnjj()7#XQ>1B2% z4Z3r^U1WOL{WPQhI(`9CKGVze$n}aLA|NDWFF#M=n?fREe0e_8gEt>iw67jB3YoxT zmVtUJ_rp`S*o8AcOLN!5f1*Qb$L8*b2LSn7BkRBnDYPh6M|0roigMl=k!1PXRaA&9 z`)4_i*+1r#8o6XYkm;CPW;Q6i?V%Hi5fNYneI z$|27B?e-)LotXNx`%89k!cl?*+Q^V@$cod%Vna0BZiPW{^9d3U5>lZ&Ij(r?dI3nS(U(3$4!TD6~;5=MYcgKX9D+L z;3wvef4EZCPaH%=^%9;Kt`qc=-KaoD?g%Y)p{z^zEAS15<6pSPvKYgqf){34|8$>l z2k&tUuY-40Q^n^)#D)3?&6j zK%C(!g;}3)6h2|s{#iK#-oyzk(H<0bzD=+?E+zw8A?Yne$NWB2bK+3hqa$YNLER$X zZbWzFsNkm1CGkmC`wA2f?YmpO(o)mE0x|h+lPz6Y>Xt5~)T7II*~f942w?`HQ*!eP zuj%8>D+}-|$n19W3cw4vBXBPk0+0^RSnqC=73X#1l3fWg@J;rzHz9!-AOd3{EX5Z} zxY)Y5k3TcF38GE%EQC(no4{qo*!`UUcu{7jUup}&K?4(khEF#W;g>rz5f-D@JpVDc zA2O2e_s+Btnqr!y7kMdRY@?Udz>4XbONF3~>#Ik%$p)FJ&%lc;elMU)yvP8l7ek?k zvF95Ky^TG%8M9AbWFhWNP#9?_m^C1G(Qd*wGj<6tpWIM@{1gzxPyk518VX>Lz84u9 zgDlNZP!euQ^*t<^*;yBh#usg=iZpV{jhh9FhSyU`5pqh+FFuQRp6YW}>UI&jWTM<# zqwo#$$)5bn3=iAkWfVDA%ML!6@Uh&@E3qvrjtTOK(H!*3LVN@vbPo``_~Ct7qp2+i z<7DB!x|@j&>L}Q-O{@GHtFRX%Zp)q-C*aF5PUBiJj1w@1Tq|fk*mm1|Za2P3IoXqT z8Em^{E-b%_vB@jrB^3X2t1ICYZ9pfcf_(G zJiEjUT%>C+a@&_xS_@;T64zc{K6`PlrAxCdNx2vU0kgGYexp{sRa|IVM|o3%d47+V zy8HZc^x&llUMCCp34_fAk}D*xTppLX3x3%!{O1X7y-UWD1n)tV$IB7{sP^S(!#)3( z)$W_WOjcf!;Lej2@p2V^<`yrn9E}n9yX$xlQ26Ol9Z^B+7v;)d>>jdw@C0N8ZC#vu zw>)94WyAM1ae21`=f#!S*5oIy-OH9YWUf-uJLKMYcshaEy z9eVMS#V<(uHKL&xFI)Uhr2oPSy?DhkU;2_hVt6qt=;T$4AN}*I#6wj!S^PSiV5pa& z0M_n9$pID~%Clj>!b8~@1}r?3*TQgxhw{3>_1!P((7*1v)MA!p4nluGNzuJrDwDItfSe0cz@9|q{IgI0(6r@ z9o8Lx<)9Eby&?5`X!uQwOTmAU8?829UH5VLR=7}j2Dex|btB|(d3Uf9ByCKFd(n#e z5G-4JgXJ&bZo>5NSLwzFGE5I|Tjs_cnT9Yupbwv2)#JzsHoe_J%M$|^cZZQ6g220g z2s#tLdAI3(C){Be#CN5`9zcB0GLg>RKtvw_19b(ia;Y6=3kd*r;8nHra;txvRPwtC z&&2yyN^}?LCJfjI77zHz6q#B4>8XylSBW0xPQ7ME z=2=;8X+1WQEJoaXT~v1BkSr+CDk2}+#+Tw@9%z2``b~Ux9A8Su_vhhrB0i`;T8?$9 zGmPJr=7&-mu)m26^G>qsP?_IIZ;&rjicwKD3*Q5e;j4qCgdMES{v9&-`9(bb zULKdncv`GMuZYnY!&eVu_Wf?_Z$@Q0}NGSGfsQp zf2n1#ljV#t9fPXG_Pi(>*k}CwoSY_HWU}yC4zXADnfOK;CtwBID}aaGw2;7OHW2$W zSd$!>|uG%abO(x|UcWHrzIVWv8)$YSL9E`h1z+5nBPD#0M(7@Zs z53&<)uo!kHdNsZ+3j|6d-~{-xM0A2~A~%@o43MvdkB!W5O2E=sIdD2N;Z(CF1t!H- zS`(bX2b5A7u!JoU_p%#{N>hLexJyzB02KGu8>bD$RAt)%^qV6s4$iCsE(bj7^Z037 z@0Q;*YA`=^I2XwoEN?j-pg!=lh+U2UU$4EnQ&nU6uao2p1QrKU`IE9A_b!{NzOEj) zWKWe=S9x`Ns>b+&<@ZGF72fPU)#ub7y$g;|&55^fMb>*U_l28l)dp9ss`NT9P}ORs zJN}k$st4RdR}VwwJEy5x>Z%KWJ3>9+U2(nI2U#y%uj=u0-gH%kpBw7cXmy?Y=x+yB zY~wfTLA8_a0w&!j#C1T=0#!xgMSXy>5FrgI@>1*^6rB(XT){V$iB{aAp2Uu z5kTnXlS<&Ha4EKs%NduBo)XjBe_n2UD`l7W1iGr+XWsCfPYgV+P>p-+_zz0?N zc4OR#)@S1O{glunnb{#ycdh?wUEHSNwCv2tbP{sb2;UCN2;UfmZ-Fm-7ijZIIC`^m zeNk5O;^`@2RSQ|m!ao#buN<<=Vlz|U%{5QLu?dgMQ|J_fe@yr5Q4ifkiT?@^E5gx6OIRY z&RU4FoJ_`dt<#C@ox(um3Viej-&6--WZ?J#PNKZ51=5*F087PVBl$ug==hau(W$ZJ zaDLqP8IjQ>s1h1L32+C9)l7mfR1CC`c77UMBl`waX-^l^p)=kx%&oXDtsZu#+_zu( zau8y`ibWF!D|XMn?^v+T^@qgCipyrH(eaIjLmYat8tl!Tr6#Ipy+e*uGZLHIg-v){ z?^i!Xm3jNCk*IR$LRI0l9vUB`u5#D^ZX{mZaJ*{biwD3DuX2z1eKisjTEJbe^G-i3 zKCoidonXVZuwgJ@Drf0J`+4t%!{U3W8@;U?)bVPScjy=6&DDNM;luGTA9mN>U!zvL zoA3X!dcynY1l0mIeBR;l5$NfH6V)KK!MpxMbsU)O+y`c;C*8{)_+EFG{Lz=!K2m?GM_S>1!9~oFDOh3$Y1=CQ?k$#cs zYNn}kfQ*3XKYjgVf&G&`VNM0zdL1CW*u84CA8kW-9%bCG_R>7lIA zAnBi7hjb;=U}<>2l4=4_y(FT4@yI=-)w z^i50)25Ke!1k(a=W{_mO%nX5Ou%zE-S}-s~(&yiVG$*Y%E$QVD5g4W4d0_ zk7UzBCEdxiOyMv|zsEE&I=FbaWL!Y?Ng5ad+69+kuD#kl;?W5e*ib`)+RWPu1|xpA zDKM8Mj~)z7?az-wQ*-TgqvI=*3 zK53v|Kv>Y&BB+AU+}H<<+l7x0*5@g=X+ybt*W+V!n{wA}81L3>s8!#Ur*GrwSb4f> z!_9Gzz}fC8FI2lfdEy8#uM7TMl^RRP39+)j=c*@9i5-XZ1S!<^{q)g^C2YFs1ot~nRJoJ>(5BBcuS)-Tv_8i?-t@<# z)k3%Und{YA?w!wEug^|O88S7UI3xMwM0N}^7#VU~-aR%`{yyC(871{R{$i?E~d-1WD0e?w)+53gsena>M z-tnO3=rmGdGxbJO?$0u_ipzL6Nh~W4DLx4)Q* zkKmhr?$`~}`eD~0+tx^Cm$_?S9Hz?LtuNNp!HF!Daa!v7@VbFP>93oyFMSbYe)>xg zXztioh8C>)1=mg7zTQ3hmBD3aBIQKwYoPN%04V!Ocfl+Bf}+;G@}-JP!VL=P%R;Ps z;~Qx3=dW@hw!b<$zUr5<5VyV>EW}NBbX|xKZjyzVl!cglExQoC8=8Q^6W$t!rdGa| zTaI_OZg!CYPMHoZN?{b+Nl;=#V* zX`^9SIXu1HjbZ)2jihcm+wsoc7K^&;Kdh>FfyJO)O$=AH-NyGO#OZ0X zv$bw}S89!9t+dpFe|KVPWUj-iY=^g^!x3>@t{SR;^?Oo(M6Nze>4~Wkx%%mB{a;J{ zbYA_RZ2zPoL(J9OK~INZmfP8Pi{g@*ZsUVVR@w})0E;}eL6&ED2RB^WsO z11UDtFShA}@v1|*Y=Yq$DtG>edxMKV@?i}+eEq}m>OmHAo3_VQuB{8Vv&{PKEc3?p z3F;D-NfwOY{@*-j~jG)&Nrc{MrYQH1^Tt+mG^zKfonCb<< z3IR|p^bRRdHH8A+QfzCGuoaUV!?QXm4` zlW-MF=JnZ3SU5rEdD%?tg9e!!B$Ie7a;x7N!gjC)8N6MXts-L7G`>_a0abym8r4QY z^g3W5#2mb9Y=h{ekLY-rN~=X?PEgdc-|`u*_lq)9GU1o}pbR8-i`{MuM1qH`>+~L>EC$M2dW{uU3pFO zV*|bK4OCwF@3R^R?sKQ+R}RN`T@6Sap6qKBeHn)gg1c^8p!<1SuTeE2kK2KIyH4K)5NdrB=-;Kd+B%uh@mQz2j@u-13EeYPdXhW0+>(RTGu|`$=AzEIU`=clXWpIIsm@T@>H~?e8XRp<=V6q< z4_ajT@6eVvZ;0Bhd{N&vJ35SqO*T6=n9YtgYmNT>scm+={ZQ7j&24r(8Q$!;-E4OF z1wZA@4sYFDReGyET>W=co#ZWSP`}2{#70o+N#1FV>c3+r0f&{|c@1iBZ`vqTDCsIm zmy*}g@798=m*I1f%v(2%Qb#Ivsdvxr>f+1=1*v*Y9Sgy2hGboSGM%3slAjzLN}gYk z8kC=;H5;^yw=`ZuNm#HE#5I|Y@tPKDC??8=4m*|kF*Ab@m4HOzbPd73761Y zelsITo?F0(rXZQIQ5rEyL7GV*Et^DX?h=J6LsDgvC=3~uO`O*S!eIEz>+fiw@&|}$=Vw{SyeFTl3%$arYPfgl1eKW! zUoUoy7+^tr20IwTVW=a%0?G71)z*gG(5E%a5+WBVr$93kZ+m z9>xY=z4+K{`~Hg;MQaCPe+HXCRyvM-9^E7^Eo_AIxFEtTyc;K~5ucNI3hhALqPMBo ze#-4}FGk|T2F_Ert$mxk0r z7{x}JExi+4c@KDZ&r?I7%sxL4VIG^rpXQZ(T^*pF^2+W~(-JGN9f&Xw@ASVQUSorI z!`D?acJr>gOC5r`f4@sH$^$M(Y$?LsSnl2R4b=}X)_+4{J<7hR80+!o-4g5Jyunxx z?;b}X*rU5=KLP!ItE1BD4)3lv)P!+i-+tD;h@IH`DGZE>|%_6|Z# zZuL$+R_%vfN5DDMYkvnrTj||!k3@i!d<$CplipkRsQtO!=zAVfpAjIfZ>in9_0u9e)fJeO z6<)VJ;MR6CS^q|Rz~1c7s}1GXWC{4Y#v1zptg$^8U=tbE*cTA=bCq|`0yQSSzz{GH z^EuV&?Q@R$d}3vX!N~;&t4mP2abd78-i%6vj*D|oeDy*Su=lrx3VY&S-A@t6B%9)c zyrX`qN@5e*UAjViGJ-hNWYQJP|XPovm z0|&fE7lFW6ds`O4PM+_LKNmec;T?FcYAeRBn#82S+JPtJwH(l&!v}W_ZsXt}4~>-n)mZ1?Bhl zjhn7As0>>{i`EG$KfREzf36jDg?G+W)pHouiteIh24DYX%(>DV_8s`N+OveT z6AqmfU0Dv@alb*#e=vu>p?r1UxbgzC4#Qk|#iO4xWBTWET|swasCZXwIFX8Cj0n z>%1#6%8SXTSG>JiRUI0d)2h~ZyMLP!q6ZbMBTL8mVR%dL`}bSre>)pCL|Y#h_+mq$ zVHIqPkCXr41wO-j{(Okr-}G&jZ|uIx*FN%ZwaQ1kEAOuKp14>YQ+`q3#Ok)JYM;O0 zHmvzKU2x0#y5O>i$x77azcV=bk6v(ZEysd;ERpWZ`Ic27ec#{m+4k_i;evblYW219 zC0WAS@z&;ozHbul?%sWO()-o5>dWPq_l?D^2(dWdS-0kvPZ{I?Yzyn88&$;t@DsfR zo8Liz<_!ESM6mZ{+(V(L;4pw&AvP4}vv{G7e?~w#OR%+Bz>Q4BBV9klPuqI$p&Qk{ zg}DP3IIs04bppG1`LpWcIko84Ik%|)vZC`-ac|=7*ye-Z;&yd3(w3z05Dgx5 zP{O-IHD=}@AQuNEG)6cKo3cfBXxw$8n}Yw~>*JAFoapR-0{m>+i5*f?VnrjkWkas) z4a(^z#O^Dnz!WM(p@N7i(9xt7iz8M5ZZ;ee-oF?(Y|!}$H_LC8xIr`Y+L=eIr7n_d z4x2HGcT*(Tm1V`anPivC$*t+~cfS0+S?byoB{80V<C=QF`KKv4I-mPj7 zh{!(PJN-U*p^x`|V}72-Pq<`ZsN}>oYGz@!mHA%l8Z{%FZhKTs@ea99#c~b!Yh*XC z>31p_E{hd2NixG_W&N14;p}J#2K9S^cI@xfI7F*__4jIW`6GSPmp1igpxk8$S0=N|fFjavk6?pR>QKP+MA64@M*0r1W!*!|*shD|p;5xNmz`e5XgoT+@#e8^N z$-}VqnmWNLIS^a7+MeUh{XK$7@YRdH9M@XW%QQT5&%fU^h@JRkmS^qQ5wtjqn5^2m z`d)*R|5Ua0h3C}`IC($chH~I3|=k~bNtwcoAFKDsDl$q zCisD;WEr(4!4KPzUz%!be;#fybK@SPJ-Eh3yFbu1;X5 zWAmTXs0yC>!J&zG=yr)XA~eUms0LR0u@4CJ1Djrfz}+~4$?Rp`*IrbEhRIzLTCSFe z=Y>7AM$-wNWKA>|c$dGZst>}k)ns$g5$R-bY_;G}In;_6iv*)a5(rm=kcEuLxg*9; zL-w1S19(mlVIY2@IpL|7RMnAkNdQN@5*G<9IGpIPLZ?6)yVg?n5 zCi^z;u9py0YI&Pbz3g_p!c_$J9g16svF4~Z;AJ&I)p)aB1_Qj+JLP3nt1nUB3l)h0 zZrjk349XP$s2-O%K%tDJ2 zb+QC<5EG@ib2Z&mm$DP&4YVbxHeW!SiBpDzj&*pI5?k3PRqQQ%MGZXyoiZQ|LLQYg zu5)A5FCu~&=Zhd$|Ar&cvr%onWQhJ1+^Lp(#Vgdzt3ogOD%7v}PA#|GsG8dV1pzwt zjo#j`A|4mQ2-RP-! zce_xZDNce!TQ~r)^))q6->6K$!7U*swj;%Ln-KzG#dW!{yZ-|M4!#Q0>e;stn*h!3 z?Jdyk@IfuaGLU2Lcr$KWcXj_I#FPG~glSy)t|~8~;b;Z7DgD}e;9WIj3?y6BUZKEH z@y2}9o^sBtVSFoD!%#-ZACSg$z0;4wJ=4Zu}h40w#W5eb@2Nl>^ zlvXN(LSUtOZSSd3Lp1HuDD6^mIoPFKg9YS`1zdyP1Mew!0+%9A8(A)B}=jLZbkV3ERoVB_jYwul2<{FAbA z{~{TPjq4!EH|*tdZBkH`Y{RtTom>=8X9O)KqX@Sw)<>L7I#L5z%*wZNb}pAXBC7E! zY?u>}B#d_qIu;SmQ{>n<@5@k3LbHhXvoH`dn_!R&Vvl5;MKtM^ci8)C55!k2cwY@w zcYD{qkFfo%-rMi1Q;Y9cX${tbqy9fl3RbGd2UDRr6};gCQ^D>n;+#klwZ?Et%(ORe zRD8g2%#r3Cv3z)rh}eRllK04aYM7V$P!;fI5O@?2@MbTFi5w!$7`Y?2xoKaQF)9=% z4Moq02_KZFY$aqdq6aUtS#npX!*=9iI~)^bcczWAq}TvQ?&t#uX#v5Ig?Gw}R)25( z2MA0;RS~#8DJy%UCCv+P@EF;znY3vd>DNu#G>i0~Oqw@9nql$sN&^@!(*rmtbGiW; z*WM|Slf0r3L=MapN7#_`h}@$__9KiaZbodC1mbJCY-vinwE^!D8dD(3Mb? zs2RZ&<@yLAk>cISup1-T*-r>yP@63zM>LoQI3&}XaU6ks;FZdDVq`IKn?Dj>&1VF! zOr8kiV00s7%21vQf`3v$ck(Pipnva>zp2^`>`1yLicgSeFzdh?`Rzu;G7D$Lva}4; z=QSRJ1xpNWLc%Ay3Q-`p5CMp1In@-<^{5?T6SZ`L{LSzWJUwj+ND$n!miOe}5C`?B zSNV7K)snYB(tei_d#iW)-&J+VSp}iICz)4KHtak0+2QO3-lKn4=cvD5{DPk0;Q--x z_1|OOxgFNni>K;->Y9r`r!Q8^@p7eJF8Ncu2bI3&;!eGncd*uRZD>r%c911#B7j*K{Su!5q`zQPoL$OB|75#`SX@J)m)$w!+ z^i^QR;6yAC9*f~x_AQ)v;n?%NKWw&!l^ZW0mZ&W*Kt4?LCRuun`jt1&(nD~1=yXd@ zS9f`LTKcjSWKbN4-KMOjh_`o%9_pPF(*uernutvW*|W^MCZY@A?~bINZeiY=y?MpDJhRZh zNs@YMyP7gvl2pdSAH1M)MyMnR zI@xkbp|-xZxNGAiN2!9lJg1o|!c0Ip2R+1x4btCbj1PP zy5N!yx|kF+)Fb?I!KM%YZi2MI-H;|1YWhf<;B6@YV!QPyH)yg91~qPnJ<6q;eve~< zlJ?53gOtlPS?u~mYE-tEkBdrDh?!BjZac7v zuWPZgpcrm%tvAJR%Ls_B&LoDxRPKpX`UmwuDnojKv^*#SG~$#G$A~LZ&^FHaNTPxX z|0-u(eu4zf_sE+%+~j2*K_XQ_J3o#Ew>9G~7?j5O9+sv?9Z7p973<_a-R65>P#KHF zzXjRJYrQ5YfY$p9(R{sIBeg*pG~(Y*>w*%11p6U7=&wE~iT^%9m*^oYAdhd1ZYiiMMD?`=biHY3 zdqImMci-Z90fNzCN;^vnvzNQ!K33D+OK{^A;2yM~>{4;vC9{q*XFJXb6=Ywp!bs>< z%gE8m!san(r8$y@fT^h~;uHb9Jog2+98MOQYk3=T*YdKl+_k*Tqw9D;n)lTe&W5P!XOQ z#t}ScO~<_v+)|F`Nl+-uqLI27XNDd}U8cNy%k}R1t`6_La(zhAU0e@hYFy(TIzWHd zx>DnGzykyHFx)!u<^VmYZYi%%Rz*igaIa4>HZ1iHb&R!`hw<~!-}b2(Ro*1)Li40Rq9G~)mRCwXqk6J zrQWaNN!46aXHMS9F**fjCzj62N`LA9>Z&nowd`zpLUrp9f!N^NgxTg)1B9LJ1HBaSaA72|_8 zqIgMp>NV0_!*g}OqzH7cWFY3??YOt^Ks~E0fx&`t!aFPm;X85f(t&#JfMi_(HjtuZ zqrt0p<6dcv-nXm(HNg6U?Dyi{H)`}<<%NC?@WSBLpS(S4^*IBI{5L>-@Mc@wd%RXp zsYw}B2ifm~c{D~lElgFy8$L*1gju~ZqWE!Xneft#BH;)iU;}q@IvS4kDH^1nw%{bE~&&y-fZDkO8joJ z;0l0H51M!$^wgj`tzukB+^Ao&>Ixkwx#oH*gTKf95KOZg>#;ze(jR!G-gBD2t9s4c5b7*5*fl$t`BJ)p|3~52_sQ(jd#XKJ+3w=NI>P-%Z<76 zx2_$jEp@yd0#)t|YdXOPl@vHO(o|fgn{xj#%YnO!i#wPXun%QtXQl#+TV}5wyk;Gk zF^l1%Unn+0l+hh2Y5@y=%MO4?}0|C_DZ83Rw)32 zR-|l}Sj)Y-QF`cCo|C!llrLTcAf&P&+$QS?D_WWRxcA4N2+k+t@*25bk0_jOZbS>( z4@i4vaewa@qxAlO<<(J`uG_rCXkCwO?{aU9aHuNt-W#LqD+SU3ciFkIWp2gbP@;-qm{j<>T?>#zEpH(Vz zjWhj0_2VY&LiPWyDi1BT0NtD^Z|-h-;NAnMl-1z6;Yb!Yc=-WjI++$o7k0!f>$K(? zkh1x5owHp&Hc?Lv$y>ggPUADj5w1Jba5nMYMXF8#A);lhN8Db(>(HUCPGN8rlo(zL z7{OC*3)BO78fI!XR|qgr?*eYQGhmW*^k<=&_vu1YDbNH{4%B=Y_w3zuIks;f-dztY zm79rh6<9Di2k#CwW0iOE?s@`FBP`urSLoYfQlkzi1YSt`f&}6z**s9%=j{-XA#d!i zf2sO;7fjOo98wmVHmYsBzVNv%@WLz=rowkRr-2L>VsNB$qucV)u+PS?V z`*cFVs$HQwi5wZ@f8aV4R8%4VpZ!m-D~$$DI{RV$m3^U~ zKPOMoKW>;8^!N4BBC`X7zD-abl-b;a_x2`hG%BTEgSYUQse0h_`=h#vgbZU)6l3&# zr?^~!Eas19Ux#ryw%Kn45@1A-R6tf@f zsdvNAHGAs4aMS*4dqTy8x;%IqXyF2HpJ}icp7Kta1_`^-y8=IHXu?$hOhJ22Kt`Te zr(hJzcj2JIS2jKSKLo=Q-U60!BugtGycP?24&@~)V!cdNz@8eZXq4vu8=X+|C6e{s94w|P4F z&jv3&Ls#JPxG6LAxSGvzfQmv@EZZ1O*HxiG+~5sv=C#hyb^ETj;2lSS6#pb^>f`vp z9YQ$RDGGJOn7tv2+hCcAuRz$#pjamuCIUkQK`2RkqxaGb{e89Z%CGLFRn<8$-uO-g zKS}d|u)_yyKjD3MZ+$G#{D-~uvU(y1QVOh`aR%+?G9&G@--ESSB5CjPeW2ki^F}s7 zj=p;9$xZsFiQ1PiC9qCk27huE{~nPg+%v+*EsAjX+p9Q0|6;J;@bDJ}AU&h$P&2wDcfdtrTJUW_?$jcPu+Gs z`g+A2dN@*`!-d8W*?wwi|p7CJXn>1m+x!xz|`3L&M({_aSQLHSf8D^}MFPF&lwO&YG0~ zO;YP{?K>(GUwFmDfdFwwP;#$~2Wu+HYx0aRDX_512RR zcr}OW;Tfbsccdk=h$Ov4CTzVb8;0L%!m9hw0H( zLQ|9z+lQgK*5ULCEn@uE9}d&?FfGAt#J%J%q@;Zrngk_)-!8;9I+{P zoO|xTs!;Y_Uh^0Agp9P^pj(I6LG@`s55|n#HVov3uQ|Sjx&E<59g_O|x;-5Eb`j#y z#BIQ9%9y`#IJgW4_;W6?sJ)QGmbHSkGl9T4o8CoJONF7MG85-ux-y_s%oj8}Q^6EQ zf+2CNO5##}y&z2{!^**=fKajUrSG6Q`z%e6WB@*>DEZsW+1ebO;z1<$Ei#wC+l3)W)n@Meq)Fqu(7F`-GjyNmL zlQ2vH41|}2x2NDsdJ*{9C11kE)iUp~FKMUz-Rx+!jgJe*F&o>wkw@sGM0glPV2InJ z-g!spvBe*5As`f-CLIy@lV5&}dz@znGjAzfIL8<5k1p3k*{JUun`TExitXClFQjEd? zNPQQ}&E$}oz%n%~)pE9(aVclT`5OfeMTi~LmJUyR@);)L&6n$eLKwK>4 zSgh}DK3b0%d=4k%OgMs*_SvkT#CJ3B0WdyTcAGc(%X(hsVlKg`?V&Av#XLTnx2mJT zc1s#m&br$%_5ha-K9B0OgUp4Kc0O*nkh4@Aumt8w&@EDYj}1)wVL`>>SwzK>S@zSC zJdUXo{e)NX6{rL+dk1|5Y}QZ$w9XL!SVHgmqOa)seZW7m9l6d zt*ygyYtU(^98o);jlA&{y~jz?MIIXw*AdtVJ~iyDdIK2$2wI(C7b_&r zp!uArwNE#kf-st>*zI41@Oj1?{58F2mL)9x3!qrvl_f;M5_naJVF~<<8@NR076BM6 zp?#|XojR0Z36v(Qy=%Uv&m~nK1I2Th_kH|S2}_`AgNT*)NM4{~JZ|&W9s?(BZm6KD z9E+rq15tp%FY}I?qlf7aqTXq9be;Y%>RmBMS8A|}+vn)fTuHCa(Ss)B+Tr5=i3P77 zW_N64RkFU=d~60_qE(=t{w|eWX zx2hITM!V%-Yrz`mqEothr#6Q)O1+=><#CmblLNXu~bzq z-x6m*FLup|LOBswbtq#cxknq5Y8*7?zvItua1qGt?W6T1R~)Zbvc~eg$QZh7ZcV zCPnWD8}fwkqc%Okzx0*(>l-V3F&cL@eE?8 zj=$$?+}bhUAM*8h|J{X`dXCc4d_$_9VXah5pAZ@t6g2f>@*766^eLIMV3omwi`)E@ zUynahK3CNAHEe=3I~0q z=c$t`n4%&?nv?34F7TIA?Q7HXS?N6Q8}XN-bG`d!eAywFK6EQFk#_rHU1>qjOMPix z&r4jIlfS$%j@ItZ-x!Qa%X?mu(z2eHxOB}!zW>yCk8(gJ(|SoBUdNr%)p5EY>py^D zR-_jIr#5HsNdT+wq=^W+Rz4~%X5N%uD8gMs6a123x+xkiDK>6{8bz~E^8CptW7EPR!qzn87 z)8Z|^p_Sv(dedfW&0}B*R8JjiA7K`lnOmW^Vjj`5Cv}C^?l@T37zw8#3*923Y({GX zpBE{~Ae2$qXd2=Y!d#dnrhKT0VU&j+J!S24c<#FXnP0mDa>Sey_u(4-6vK-fqTKIpnIJ{XD_kUIw)CY*Nn1Bv_9&FuXjT4JW!JBfrHCmY_msHRoi zc-aUT_96q?hP)lsfjXfM+jXz0ZD1q4RxgWqsb|J;oqAOt(nJwynR+|-ue8##xO8bH zOV!6W_{dds`dAjJ?O;HLL{Qv>CW=B8qQW3vvl`d)iVJ{d0Vr#hIz#KC0=N9>rRP&9 zsj+~D&{4Rj$P@`|6Ct`ha4O`m`kIP^LQjowuumx6pYPTOIUUHTUNnRf)>>w}Q~)tn zR+B&?^M;$H%RV5L;L4-VGSm%JL3!U9 zp71&Ah4hKKo;6;JKgiN0CN+EUA;VQqx6#0=sM5Wx+h)6D*&T;oqJzKmz4*S*&?j@V z;j7Dgo-Q-I8XfH?SM04|XPr!MZU+`Pb?Hr=aC$V%kc?6>B#u= zey<aBPLwsOGn4!Mzgk3G!P4(qqFHL&XLLz%8a8~L0IPh ze01D4c#Zg5A!gzt{`UlLb*Ll0i@xA3??4%6a#FI+A1s zKe@X)R(5?_N4|1t$I0;*d4FQIvV5Kv2+fLVf|XePW-Zw+^zK)hO^{zdW?N`qZkb+K zjK3@CK$0b)T%Aj8?eUyA-C8Gr6x;0R6IXirlKCg)Al-*puKg&M!*`NBe zc-THAnr4MEe9^711(p+9V^)d^9d94^DGz#9g%M&+Y7xtc0XqmOCHMua(?2;bDSNg+m_0TgRF(5{Se% z#n13)XA}XT_ITptcp|@8V{PYwe_Ttg4V?Z`pRXZOU=S`8S-raLI*tEqkZXanEy$?s@V4$4>o3Pp=W(+q8!)LzW%h)6?s?uU;EQ zK2Wb~Cx5h$Uc*~C39&ztj??e#n)S{(^RF4(ao&QTzw!Ger(UyPg@l_27t3>8^wHEN zQL%h;bHSFd!FzJScTR|h_TS81Ut(R4Vo&A6$`xkcfb4d_`er2iCa5kxljMr*e$0uR zo#!vi>HO-$N6&hE&Xt!vSLpcJ^5@=sXZ4$JetDCGzHl=W`lCqVdlI_i>IY`Luyp0U zPt7QI+_>(Fx9&am>J@(|Y-Yk=o#Ud<#x{wJtF`dB`8Z+!z8HQ(1u%EvujZe*dd}G| z7djqUKlj>WFTd^1>&tzjBODsP%>VNAc$>dZ9p0RyqtC{Aq2tyneaHb!n{y~=wXvWt zoK%K_^6X8B#XsKn6}b+e?jalrk&k#Nf$F|;N$}<*eP6wV@{@Y${?ksG>E|pv{mpj^ z9bHdfz38_4?&KEZ%}n}V=cMnmu}z|bD@Zpd=|A$mr0;m~{@-<-e8#mW{Ab6F7o7R) z<9~g{ZS9*G6{qK%u)`cUVN;C2CAI$tjL@`Q)o9SM;GH|Kn|b>$9yqw;rZe7s>yMXQ z_{^@Gx$kpxHt3_NO)>$;qi;?o7+36gzp>#DC*C;sgd6Tz)7UZl%!OB+GUK|HPi}63 zI5WpYUu|s?72F}RS-E1rO&AH)T(S7J`Lk!gdHfU47dlQjY4HtR3ubk{(kC{Sa!U!< zu`lySoe@v``$mR)b8Ph4STA%euNoORg>7?k#=h^b913{J-1oV@XYtcDxUc;tqsue$u~7YZG}efhRa&%ODoCBNv? zYWOF3xxsa-Ry$0AV)@@jcpPc z3pOur?7IoOqL*3m$hl4{LM|-zRkyp6<1=%SzXI-c=ivcJh~3o$JH}0|K*9lS}}byOZ@Dd z$@^-n7n4_`g8L&jE8$t$fz5k6R5$P}_(Lz;>})BVtNpRUw@qW9f7 z<*B^)hJkch3+d@@pS(2i{s&S z)jc0KjtxH9a)Q6j%&ZD)ssHj*(A-mudiCzllNFh zi2b3*$D=-h=xPd7+n0IAN=K&8S!XM{j+1Y?e&!j^E?x7DjwLtEzT}0QuRXIXAIMqF zw)LL>WCuf^`2xssuLYkQ{!W?&klQklH|^_<{ea8j;q^fXYq*O~D^U)}{J_iNA>5=c zPh4+MuK0Tqx9#77I3Hae53PXniym-DAF^*>;QZo$4KvSQUWJ(ldq8;+Q2NBoga50T zdGd;QXl)>rfBt82yHSb6zR_~gKZKT!_h%TGd&TUxUYv2?L(BVhEPwjEhdOV6?WFOU zIM<}d8=lJI?LM;gMZn{GW9^?_(9@v1_j`}J9ao=p-VJ9yyX374%N;k|e!-h}{_X8M zxf48>g09d9X_P)QhF zoU0hl95JnW?^S5k?2=krk{CaZxX?{$)q^vwx~fO3+M0A<4fUX40~b99#8#`-f0CMC z>r>gnq}9fA$Lg0KefP#oR&^fUF>A?Lr=EM(vCp(th8K&H)ABrP&mCI0?<7v*P$s!t zUS7(skz3}sUlNZWzxq9Jb7+Q}6F0z3A5Hf{&5czP$`dy)i+q5Unxn3bhi#V|Sh`ux zZHNhNL>4BhnPl8}?r+^BmmHp9;@TceI3&f>*3eC_+;H0RW@L*^amDt%*kbuHM=yA1 z-Kswv(Xsl?TP}U(wl&Xxt-=>0ZpQ^&Fb(!gF zPk41+_b;yg<9~FV_r#kEPI~gp*&nS~0e6@cfVb$;W%0g5@Dt8;y1o z*!ee(ptI46vnBSaUFTWU6vyqtK^8T#Q51bmsFeasVNAxTQg_S|7FnE{o+q<2B~Hsv z6Mii#WNST2TZ=H=&$0l2@o;xlre1j>!$b*F~CdT&g#oi|iTNDYYC%5FTgX zalSz`m7p|^K~s1%v4G#TBN8nvk5oeR(k5O1ae;j>Yvc|I5`@Q0cKQJ7!A zvqq>RX0WUVFsjmlg02^Qg@_Z+LnJRo9YKw4b=lS#V1_^nDdKqJ`bwgdE^&Fp@8wBi z4T#ujA%=HHZT-WM5^C?y>hb!xJhg2MQRQ{I|3ZAy4+^OWhdICojO<(T9M|);A7?sQ z{}u1pI)YOWY6wNps+?@zh^J`pO@^T%$&}*AeVdaQtAL*ca9aaj2NDerQU{VW zkR!C&@j*mlS8X^+vlO+C1lG7zXoMs~^nD1K3pQdEMRq+*r_OT@38bAv#2R!jEX> zsQT7g;Uk>lS;v0YnC)DGiReVaVOx@)*U{!8OIu)Elw7ad35#Mw55C_51_ZlsGe!jVSJEu1}`z18g3JXq(C$IT7J zMm8w7amo(iqxIt20#a8 zfQsK7z&00M8*%>wff|L7>tc8@DSAEY_E|E3mnFfKKR*OSStWn2@Pmm>8~?reY&I5cH=gC}R@p z72VCB)WEi?kOhb*cq@>r>Hguw%6|K^d3uA2P*0(va$XZuP%ZpHl=J$G1G!B~;Bx$} ziVXjBb8|TEvTpz8WW##@J^-7VlCt`Z&Jg~4b+rQF`vaJjZC3@8s?aoC@EYFSCec&| zPmN(OV#EF@r469?`})%$){9Gxgtu>4(Wbgn?gQaj;eV(Q%qagY;+3pWc%8l(^O)J;R-Y465N5yLg=O!|d< z!>5+1N~3veK{|}_2UX6%P()XQ46gF5*j%RAml1{?Wj_uk~(_!qRmz5Kdt%z|n%QmwnsGmfGu6DqM z@p5UT{&NDTlz>$=E2}!Mva_tyv5Sq9V{0tb_cq;2U#@E_2&v#*HcM3ED2;9g%_7)X z##8Sc$SgFauhg}1Vi>3>lo}60y$Yq#)ICVCpBlQ^`AQ?;TvTCPc66bQYB`c`DF>(( zK&egvl?4sR)}ncQ(p~V|Qw@H5dST>YoW3riA^OX~D{LQ2MX^~)y04zI6GjA{bCcFV zdd^8u>^x^Ct+4ZR{vE zk+A}EIFG!tKZ>)X8;e7!a<0n>XLyQb}wONP=&@y4oN|Q6i06$TwsQ8h8P>nI8uv=bTxkM zGY9|>3>LuDp`jSQf(eLPF*@}@wk6PToEE!1yP-E}t+dON?h88X|Ii(M3iZgk=|h>X zHdO6bH$`c$^|_85 zq%4CZn`M5~ngE%i98?^v-PqbFiJI6WI)omj`G^R^NGCE3_9#dVTc#k{H_picZ(@RS z8d*?-nbOlZA&Mg;rt`ii?pv!D_=~wYm_wvY<kv$ z1yC^=zF}m00pEimstzc6n}J?_AhbziYus9I2gR9Sph$Af!0ZfOM>BvZ_SAj`Q{V8*Rpno{6*z$k)zi2X){_ylrHE6B78_DU}N>VJ>$ ze=(PR4*x)?QI_SQ8Q%nTprJPJ9rlKxYPYcu$UjLD9v|hQrKlq($GhtLP9+nciSo6; zvvRe7y%MMHnagD+4l0gC5E4e2PlkCbUr7}RGsj{J4&lc<5)i-AI-c^(aDrv9UnaE< z^o@_lJCq%K%u%T$K8Q=^&{p%Nl%m!IQHV?Ckr6^|Dl2?e{4s3w)e-nH1c3?o^hKDU zCX|n-^mt>+8@pp*slMDXpd7*9*c~HMNdiQ1=q=L?bcG|DO(9AJrhhPooXsH&!m%_b zIL3(k_kym^;kO}uPj1DH>=;@3ux$DrMphdN+S+z;Wa}YQ+rqY?S%uA`eAPcp^a zx>_9SfB8h*=AW2YTUTx>)YSHmvP&7z)9`Ln|9C(g*acQ<>X@@M zVpNR$lqXq^GbrDV+i*0`;kVeD#hrjQ!Ii#am;yD%f3uS|PG&TmHN#=T)La<9o7NU2x| zH4B3V#ELD@E)L8xmB`%wV_nmhK&D2P#k;s}C@I`7Wk#}0R82S$Qco-|8rBKFT5F@M zo0>j}%1>7VMJelo>(zzITj&C{o4Ll945@`kbG}W&L+zMy3bGjK;^3;rC=l zVcrlXW}FKoVn&;nz>LgGj2V*(GZq3Pq6y13wUhs&S5GTWzVr#BE{Y4geDhorjYVsjGMY$p^!d<#48~e?WS^Zd~sZFmXua#mbtA9SOR|g zY@Tnjx0*}u)%eH_8|Uneig=+Dn9u2a|9@0Pm{;|^b%XAAfl8ZD2j%}QeUPaGu2?X2 zARsEbx{8uz*(DfhhRf3VM#f&O>X4F(Iv`_Z&}hpT*MwYiObr*DFM|z$kJv`25^L(E z<4%S(8d63%)7VP-rIPxQ#9CjpU13oqPcppR9$y+Ejdi_+4I-oCE6lJ!z|w_x=Oi)_kzvSSVlHY zO3%$1!~H|UcX3^k3`s$ONh!aWc7g~joVfR3!go$2tPCJxcbqPak7aScv8NmwHlAof zKh6fjZb&1WYV1O+ynN7G4w1sB!N?$g2emQu@t2}%H`XAg7Q1@2IJfd<4Vq_wBd!43 zMzD-yx%tF6)@~K1w|x&wu$hgSrNzSUh69t@?is9Z&`TadA8GMfdHrMxhR1p~g(=!01;tIT8w2$RjS z$|NYNQ0OrzB(o&lx-w0HPErAK_~F1Dh1Sr){WTZ%8p`?yJH_pK7J78?%i3 z@55(Hd&4c0u9qNVDOLC^1AAE&pKXfG%CM||~{*Og9IPA!><)AUDc zNE_&)zIacpC$shM<2?=~qD&9(&9ZgC9^N}KesA8Re3gzU|3kdT8C%j3RlLU)Snnak zd_zf=ZWN-lNC}QIrCr__Q?fvF1H3Y8crOE?m-@0Rz-cTOaowi&9{i=#r-CB8)qtxA z#(Of@7C|mP2!m~szg|>(I0ve+(I+zMN-rs^RxSSl1`D{{q;BgOe=1Wlg(OuoSl0#& zw%~mjjPg|s_CEYo?pwJ*#SIut&IFkq3mCi1{LnKuKV(Y-GN#R)8&Pp~q&a#|)}Cn+ zBX)5-BmSI7w*RVZp7bD3v(z#JM7{vu`XYOLU}BCCt1JCUZD=XGhmAKP97c9s=~-nA zW1|*w(6q69wK*)<5zXPZnI73R1VdvnO=`{IQk!p$Htjq+AuNh{M6r#FO_)Z6F;PD6 zP=!t0N~;)NdX_Ed8-8}TQmS!yc0aDx2t)t+H0lkJh)>(#+-Xpkt;*Jw!!m8fYcRMp zS9T-^Z!%};V}KpE5Wvl5!7*T$`ew804E<8Ti^Na;EVSGogbEmOzd9*v`55*xx?> z+U!UBcRYOT?du+W>(W(w^&SD4d@$F@wK}nraEzHZ@&=MVWm=v9e%-DH0e-)1lnYH2 z%DWeiRNPCY^E&F$V4ed(I$>vFV>SfQih~?IHm?edWWX9WG&b}i2zft{`Bm<1C&SS_ zkPu5{&_Y~!hiQ|)Otm&@>4VvqLTf?ZBpYI>O`}=4r#bU)VgmGP4wLMkY>rNBS2$9W z!9vB9{V8(|>)lG7Mq4ND~&haniUoBym>9n?F9Lknu zwYD03AZ>+&m`GdG(1X~(B2kK^h$yAT;Eq9;@?WvmCJoqW{8mh(V<8Sx#F-afp5pSM z>ZGEZY6t)_UBl0!q>sE7O`roV-({^uYa>iIh`UpZHBFY;9JT~_ZB%hqZ)DDSr}_ag z&f8E-+PDI5WfquN5vZj+(Hkr#9RPSWf*Kfmq6cg27Gcux-)j}gmJlqQAlB+qt8RE9 z0;h)X8Ja5a&ze+Bll6hf(MlhPCuHeP3r#wr@2Hb%8_IM%FT%5~8skOOPL6SYXxZ8v zUrcczPr!J+Gi)2i>jZm!z_|KB$9iLYX_RBUf{ij&X}O~c<#F1{LS=81#{uP5c?gN6 zOi_U~n`or5v4ovKk7Dr!OQk|}9hq5SAuOg=xE5C!5rjl>0SmthKoO$ZHrs>f8m(cg z_d>KH8ig^DC1a9}A=XNs@k$1GM)X(%nwroubwR`;{)r_STR>@R6m=Nwsg;#%;uJ8z2n_ioHmi@Yl%);O=kW6yw;2!j9~b(V&hjvZH@d= zK$BK+T`8h>oax`TFtfr&Y1|}6>f@u@Snp95y&W)0mj)t}=`&hp5$#JfModqnHAFeF zSwpM`9OCVj{&ZwQS{Jyx4jW%=EwOg35Hm1gElEM1MVu3(pg=PwH06Y0RmFtIBZ!1# zTl`(2twAvy$T30l;?P+v92Ofw6>+8%h%K}5XNy39kcsgrgLi8KtW?Xt81`-u_8d30 zX{P|nj6!XM+=h_s#6mU-VcAD`o7%4Uo3K6LS-2>)m=URvK9|MR7h&4;S$oM3`ejQ7 zn3)@=YzW}q6WfE3u2U5;_~Fw|wyG8&6OWR#DnvDIdJ&4Vl^0y#85!qaF-4As^U^ovfhrnL6m>?o+g;ENW0PCf$9P+U^CRn*XWP_cqkZBbrn zRko{Mc8_rRslBg?)AbQ-%61y+-l0Z2ctAQZhE_x;CNcb79p|RMAX!y;Lp4vJlY&6O z`;wqQNu{q0gj8)JqDwgfJfMWwbm9xY+-OWCRWD|v4`qvwX$w`z4z_-Q6s`1&upD^~ zg2Ya?=0NkNR9W{g5W%8tL&VW)+YnV~+Ypb#LZ1B<|MQKI;VU7-p&o(Z4C1^XdTdO?ZamzISpRiD|t`VpFHLlK@uq^A7t- zX)2;lSGpsvh)5$hEdE*jsa=GJDd4T3H6l%WjDsp=3Y?nn{K|LZFKpb;vH!p6=S=J8 zf4pBmk@>hZyB2G60H)qhap3T&=@oDs2B-csrH53K7xzl`e`#{1c=<}dTQkvNbNu(4 z-N(5i;+AGNV)&m}K)|lmpStwVwhx9ah~o;r*xzk2q*Hc^-K*V5gGH#EKAiaV&2H$2 zE{b#ucwlK-4-q~f9mY2C0Ju}In1D-v()192V}G|_beWF_xP89)Jf@u#M3lbZbTK%a zYT!m5qth4d6LEy;`s@=Ig77_kX~Uc^S8~4M8m;fTI|SP)y^^|wo~t5%YvjHfJ>s{H z-LC$f=iHXj%l?`H?iXBR@{!c-8F$|AXQXauw9?N_-Nfh>e|ze-BhM?T8yCMG`4IzM zYZUoC2D+`H>-?7oy3dV>EI^hy1hyRme~Wa}jLsyz&)+}LUB*2UorByyl<6Kspvc-& zyCrgOM*f*W?tW624R-UQ8~wkx;5oU`|G34n-q7L>jPCSxL);PZYmq-{h&zSXfkWMp z^374>(Q(wT%=E9bmN^bHZu}Kd8zj@CW_qg@qSDp6Jq{Q}Yu`zxV0ch=| z$HoX*@RIC21*X~66;|yLLsBd(Kj^Q4nAMupZ`s=%ew_|O4sI$$vV*k*t{bHh# z&Jsle=-kOhV{w)PXFI*rr0eJM#{(vv$cq!nrt6!<7h)wMk*(`e@4D5QekdV5!AKWA zN==JJ^dYpAS7r@a-5EgE+TOIC!y?uJ3hZgCUiH#AzZDk?eZ>5Y7sOF~6b*E7XKs4z zlEX*1pGHH@ZBP!pWxAzTQ(l<%Tpx{cgZHve!hVh1irFV+$6(OlY4c%aSK4Arn^j3$ z^661-SoBr- z1)gQrtys9W5lA`+8S7g1un&vFhqP^^J}9f$)osuhq$K1U-ZsiAIC!iZ5-M03J`~$V z=M}63cW#3&A|xSSI~!gEK^G7N&{oPJEzoL;Equ6hto!P}7$EB@QBxcdppxN`^u{_s zab00e5tIq;^}wBM1ovz%y{ajWqT=#6H{|n{bG4<74#ilBpjdB$fCNmtGyGT8gj5_Q?!0Vuvc1+3~aOoIeN2BT;j@jDgkWk875Kv z28=|1Elbu5rWwWhksEcRn{G6y8-CJOIlk8iz=yNTLHPdMR_>($I93Ih%Vqe+F5DR3 zt1U+cIM!|-_+AWDM7IXOJ1t9Z2|zyr)PQ0JIMQ59HU@m9#JIw@Wn15XAm=R$ns6CAu5PC&;oKK<>XYfzi}T>0Y}*IjFO()P|tW28=;DsA!0d zL>E4=EE%Ur^YoHsk#BZ@yPd6Bz^#-eZX0)G6rflPWW#gVQ7s!Wc_&~F&zT%OvoVja zv>X}EQ98*+ywU}b!}ETifsEm|u2Ml@$g+ff@V$!Lm$XiF2iBlgzVjhBPI_t1huvQ% zGj^iTr)mr>G!B$CBJHmfJsX*j=ml*RO)pJ4thAb;dn)IrY3!b8WA{lKIBgg)JA$!W z)+}nCAu20MWwU(4PHwC6-z>gPvqBBmOsiFr;p}M40|_I;QjHByRv~`B%;q37^P{|> z%)DSS#OHVlt<_-C*c^=-XqsEU8MY8VCPIqdCe_N8nSC%~rJ54i_)blErQ~A3XHc!O zMqGMQ%>S*jzGU~{%+IqsEpGS5*M`A>3(h9plobZX`~VRrL#%a zW5$89Jp%K>fj^HMe^V1R$O!AIGb2o#gVD`h?lMUEQ|`tr&Fn7mClfKz1JlCzD9|$w zNwN;BxJO(gKeaKDtTdIc*wFU1A73F3r7GkRBST+G!4>tyl;(t@NjQpoW7Qb`Y!O_B zsaVN4v4N7Xgfo7U!+Uj*!35+|t}d`mKNYXnH(^WF(Hcw@atmZ)8j207i67|~s6I?I zO#EsCZ`O)l7;X)r6rTQmf8 z!g%)oZ2z;}+*a}2H~r=nl3tJuNf%Io_v*=V>`FeLj_O{$} zyqbw`Ql@uVo%8*u-QBijA5)8y)->4^$(7CIl*v_<$l3HM7rDGonW&mR$k7wzF;_;a z3r;{DzZ=DpyKW{w5k|ks^S>gRQvRAoWOZ9z@I+P9HmBG`(-r2iW(7l+AF+qq;%i*!L0|XZIVRFmlbsG)kGIM)d*S>R4*Ft@#?dcBu zX0U9?vymNvbC1Lg4b9cEzcy|}x^zQpl9Vc|z#p)e`)-Fd*VR{ZvV1pg4zh00oPT#O zH)N>Qv)Ul6ZeyFh-D&i!dvDjWU1*9dwHtLprf6-iPWWh_yjEG5wY?ypxQ|;pF(8U> zH5;1eG9YVuwc*n#Zb&F0JF#X%%$Vo7rdJ!Ep5h)2y~2-1`&IKrUn;qleXM;9q~37L zIxH%2!wCsSTdz(mnNxCi#)GlGBnM`NWdi2E{ZY52X-sppA_%7ix#os%;`MsoRx;9P zY%xKn-A=G*V%}h}UQh$(?mE3Y$eT0-G{tdSl~fa4D2vI;p2(~w*7DPx&?7pgAk@uD znG}nLy}U7I=cSd@Eu|-~#tJ!nr|j!aiq2ee@4oK3Bs$F>`w6^l^ZlitaP8b7i|tTA zH{ql(FNU=A&;7HXa63i|{FnpWzQa22zpb!cp)j&kSE@^!5IOs?(*O7Xw@1zVxbe!! zzt~i}rGMlA*FRoyhJWS&_u>8SJ_8sQ#_1v(fq=!r8bY|sQ)d!wS6iBOCMb}<0`o!H zOD8G;5y$smn!|7IqyN$;-N$R+eU0>0e(@*W_!hi_=tKosl;}1$(w5Gg=b!nc+dg{3 z4?oa-u{kO<7udti2`;7OEBuTD-H+m1R`|CMbR%$64*it-uL1XJaMcF!hV*v&5bXzSSYQ#*WdLix7BtxN9pC#QfTk0v(+#~ z@qMuFq>mtR_ksfZ*7?x~0Sj)>{SI<_4t_n7Ne{wvk8XOEKkXp*6?~t6J;-grWB9@D zyp|<#Tg`!}IFJLiYo@Wp!0G1)yS=09ebcAi_oJ>QM}OLVa`@02jL2FYiAq16E;e)1 z(;JpN{Y_Vjq8t2H)7-K6L@%9YUeI;ZT)FuN!Hw66=1<1?HAlFe{I;$Anzx2kW<;E9 zf*+izMQPqVP|lEu1Qy-2^d@w!gmIkz=3(wj(Gvfg!`wFbc>i)39`9TIhri`M({d{| zK2rj)!pSYgXd1rjrGDszipTIyf^mYYcSEx+wXkGei?C8nyMxxb6G=7PBl zl1Aw*u|MY9?z8c&SNY$6o1vDc`;OZ+y2G#-fAMtpRX(QQb!8s2zw3V4aaY{<$uPze#^ubv$T-I#9@uA2 zE_Bv>;0v~^7bAip?7#p%i8tbDSPAJJ{=dKHUPc5zdxYy4eaq=cj0w?jJpKc-#Ts${ zttJsl%@La z@4J@aJ!Md(d~M0UuKT{5FzAenx(gLr5OH$WMSklaxRd&s=`9&=U$lj*WQ3Z*usF>jKJCrLVD>+~L9`y=(-ER1wr>?i!#O^)vNhyK{zy4yMDbQek!OMlU0c~rWg zi|1`Fy&sthp_6Z)PfB~~?gc!$&?^`6yev-d_h0x4JZQCyT5{S?+?mmbZiySk!#hS9 z4{K%z#co1iN=x};+KuJuQi8HAj6ww+`af>M-iASoghD$NN;Bg0zF?NI9ryvQqjq*K zw*y6z($>XD1aksSI*K=c=l{6RCZ9I~i{t31L`JMsS&hH3 z*l$FcV6*AspQKcrE($^D$6^a^Lp8U?rFRRh1uRW^-G=jk6CxHO4rua8-$f`N#-4Dc zhL4X@XeOw$vlctjf5l`?X0>X8rcn&4NRCZZ<)7NSb_i!)_Ei*^(60bhrCpgFqvNGr zL(Jlg=;OFadTZdGJdAp#f(wle2H*5_P24ynii87{4Bg$m!2WyxA2U#zH?27JrC)F5 zFPh;-Zu1+>n1RWuS5vb{Cs9TFE9=s`p;K6dobRtF_S?A$tUVa8wq3zLd4$`=fAwg0 z*v1-W&C#x9}0TxaKN9{TR28o>v{i2rz%i?~ifc=(pw5adB%5j*4U8 zdvF(hn)co9QsGnS8vo@J-1wn?Mnkgt25BlO3c4w^Gsn2&1ct=< ze$Nx#87p1z67drQ1Cs`Jq!>w~N+! zf4Un}eo5u=y^A|iduf+{s5bn@=43K2_s@&U$9f0f;Lz~eh2f`fRxE_AkU4zATBf99 z6UECa$=Wa zP+Hj!hp(%6mDNc>6W+`Rm-XL8kvsgI<3&hWFD79m>>z9AV zyD@dIXaI}Tb$-BHhSQh)c5_{E49HN=@-PJW5LzOE;DWRz)8@J_#`{07Q4tFiW0#g+ z&>LYd;8s&}P>qkm@CQ+7Go%oVL;euCZ=^_H^tW`mp=HbehS7pxj@0Vz0->7WC!uo1 zIT63dt9@P1D-AHtlEI!FHoP&e2<0HcGya2|^FMSt%+H~e7ZZLvBEIpb%%YyI$=wq;xg-lXe}n8L}}h)41XYySF8u`J(yM-hQtab zD((2U&vk7dVlxuVMb)nhJ*H*03R<$5c{|Vvs0MZjpbyca{-7>5;XPTf!gCca{9j~2 zR|ESH7;zAYs;gK$~;{)nBf{i$;WIPQc?i^qZV169}AD~^VnxD<~aw}%G1D-9w$3F_VX7xI;*MJ%92}sdPW0DV@Mdm zb`(1;r^wo~%Crqh&+w}kF=3nQ2VP_~?;Rdrx&&OzxX8LQ_aX!2`il(Y-(O^aymN^g z<0o9~nkl-?rEX0B{v9vQ{6*(E&(8g0?N8IWo(jhFAJeh^vFlf^zx3C0T9kJ3#aIMq z_@x(Pzn+tkdr)WHk!=zqrJp zx#AK-%iWh4@Ia$)l+#0P4;bEY`)RNL{_+=Jz2o`9|4`>lb|j85R17p!)N{s}5SF82 zD5$vYQmgHiP)+eNql%&8%a<8j9C?{Fe#T|i_;r_A5B?PL4Z1wfKHPuyayJ-2re1E@ zPq^GLciH8ZedS^>_txcxxp(w?>nrRx?F{z^USS2lbcJE+_$v%k7hlm+@Qo{oXqf9& z5DdL?$H_Ooe%Dp^{yxXhaR150xG=#18!^@0TCo7fER2Q&=x(8{s7GC!x_jn;EZ zYgCybZ@BNc(kl7fl?MNjS6X*ZyVA1Xa1~7t>F_gOJK?sco__fEhFx&g;`?3YhO24u z&g82MVV}IpYWUVwR>SdESx2ZrlJlqV7<9G0?R~Z7{I9DGS2M1*oL7c#kB3M7HI{Sd zYYaJuTw};N>Ke;=dHD8dc+_57B^aEXF2T@o_1t+IxTmqZX0q4~hsq2 zs%z^HdZu4%{hd!;K>OS9K+aoydYyH2!gcN~+-W1xG2IHK$)ie&=>os|6894f&+C@B zZ6_Tg9{_4o>g?SDj&A8jP0`rrx6=GWvdwL=f+3%Y>Of8eh-Uc@FC}X2R)59yZaY6{ zDZM@354xUUH%mIGEok(`Xf+LbnLkiYDj8L!)zWjt8{F^#Cpwy9L4!Kcv%m2B-{3wr z@MOm}Dw}$+2tx9F-*p2v@+`mR26GkG-RQQVJ>G`;PaIy8`hO=uUaezqbmI@EN@fy= zw`u_ewMv6A6N(B50j4R_DhUMRXNy9u=>B32+acBc^cRAOg%-nYM9jr^3`V*)hb9p`Kb+OT!4h$WYE_Z%XJ|DEDJ2l;Al`?G9jrSuE;8b z8uBhX)tRu`C|DVf`ze<~xcOG2Z#{oBSkHRYE8NM8ay07o%E5 zYx(pxw`H9doG~`ZF1NX{U#e)dHp6`7RXQP4a1A44?E1{KmI@2tcajx-wLPiX;*E zEBTE|au|mS#2LrNtsx-azxgXy964Wd#JZosE(cv*4Q<53`u|w&wrnJC zA>(#`>~c3rA!kfV;TYV5DM+hPKpMylDZ_0VV=3Y)@)SXTmPg@tq_nqbL!sp%)I?B( zm&Lqj?r&eZd68qvUhq;O4E92x%6iM^!Y6{7$d|n+{!l_=g%lPiFHWe=zGN>7TVlc= zzRae!njD_hydd`BOBdNVtvtM}qVsX} zlC5wg`HOok3oBKN^l`7HXQi&!9Cm{>T(E4ZRqNZzx{^fBgs4$Oa$BAM!5!{kE#ljm zR2u_toZ;UBqd1Fl3%)3{csuuZH5NdbC`Te!UCAGg{ZHW3_a`=5Ni4;s}BSx-QbzX#dlZMFMK!{4oR_^!= zk9XbgTDQgRpoQHwhi+!9(8eLhR(7qZHl_U=Z9xZ&k4;)K|A zit~gZmtelKuSh$-G!&q2irObkbH~fX!Yr|L(^WQDP|SuxX8%5Gn(zYit!qHEn zaF5zQa6uk3CpM@P8`#2P-m@t18Ide>C?C-S9nca`EWN}Z`j8uC8wO>pTD!f$k@e1(Ql40YW*_;A54(w54) zLN58{!z?lk%SDL6TXscfwtdH+y~=%UM?|JZ{G#x?ATs2^jIatCbZ4Bt8KV;OATFNo zCqLr0&UshdsoVsph&KgpFs)1gOp1DOvnD+j?w1iUmZcne=cZt^S@Af%)D(oo2cfzY zcaIjBO-@Z=mb(38kGO_@$Ht>nkAMCVx8*pbfI0STRu89}LQqSlt`^y%G_OROJ#uYSJ{}LSrKVV!4SS@Er;*M+xiq*?XI+{?o_qPD_QcLyan4v z>Q_GM21dvFryh0hl)IL67Z{cdUARSp7^YXacuJ}B+HM(Pi%m-q{AAKl$*gIBlA;u+ zlNd6qL;`|&1hH2KEu?8$_EXj!hEE>Fj!juk&r@tF%PDytnV*%aVpXp7WlJF=eiF*d zV5=Ia5OOKQHUdEKlz$XND6k%gH21&8>gJ)T zGizg^6>0@z`J09xAu#lS`UE^CM19ae8Nfm*waJ?Tz~vZ7tqWQ_#+_~(ytdMiMXsHn?S`7f?HZaY=@ov*(^F|ooQRgnGUcstdPQi?S`C5f z8gjC1jtICAIStoqAk5PZ|5feubgJ)mLNirN%^eJ?xl9i&XuJY`6nc(K=8bW5ATqIu zjxe3?$=8_wtU(;lt5m6p6m98F!VLZf<2jTL-U1+~m#MP_Y)J?=lVh>31%n&g+OM2x zsm8u%JZrB#JbOyqc&!T?7CI|DYvK?ZhHnGEQzWG94tH%=I5bgIU0__fMzQ6tlTwTA zk@06m&@WSaL7s)!1B0R2!z>Wd@Rf-P;8U)`BK2w@C%Cq8qB1a3rn4wl1WR@7D@SMI<>xYt+eiJ7DrvRne~(09J)|olp}8{^Tt&5T^NW4yUu9 z{`5b%wmPsP1$&k6{zJ}ov}r(VAd%}&#Ix~H&X+JoKsFn^FfU+Df&=n&01m}$ltQ)S zG!`NtY7j#kfieVfXZvZV1nz^ zeesP6o|s4So$;(>CRh^g+u|AE0=_EkulxWKz6IDTV94SCg7A$2r+2y9Fxc3bBad&^ zFImu{x?lMx*D`|Ta*LFNbZPz@bfg#fcmCu??MRZ@x74l16eki^G1iM7V26o-rXS{d z=gu|9yySpCyHWpTs*;wb5af|fPOLpVE!1{@L24FFNHVKD^uyo& zXSY|p`WpZ6p2^-@c8iGS446|JMuhc+%DD)5Rll^BHKm1q##&a-?(~b+x}903dt$8{ zHuAP$oZU(V?eRx-vSN*XeJuTzAMgy(o6G&h&$u1p#RNY; zh3Mc>&$^eDYru1^BkJ~_dCrY*yi;|<3|cfi`Z@P6@zQ#K*_+W;;b4yI{TA!oZqcv( z=hnHe5LXn;y2uMFjp1 zf5!7}=N1(W0Ys`|96=Z9&Hkz9-Bw#It5+l~Is+}{(ojSHICi!!BzKO~$&G&d7u*=m z=s55NR)cTxQ(thOIN0h`4jhpb?XK4xHMX^+u&9WoajV)Dl>}@4PVfs>8+o{GFwzXK zJzf;0-9CAd;EOx_0WZ332j7<6534{Ks}r36{6#m1?_<`p;CP2WXuTU1-R!5WC({1c z{^Ip+V*llaprUn=o6S?jS1J=4E#t!RmLv z>{_F<{R=OQU_cAcTMHbXpQNXs_@MxQ# zncZ5g6npJ14ocV{$sth90>42z&~_J#VWoF9Ev2KG7bs95H?_3oQgb!*FtwqFwKB#C zf>C4j6m4fQJ5@q6*sZOSQB993#v1Kbk6<$QGQ840L0Op|PUY^9E@2v|3|;<|H(YxQ zm!KHF^G~#Yiof>_H*8noKI$P}KdcE*tj)se7(wBk*@{=yreRa_TfFJE3aceE{Kxri zBfOCEJKBOm_xtjjZsHi#uhg@QZaK^vqpg;^-gM)mhy9Chy5XZ2Vlx5@rz4^KQCAv4 z&<{G!=%poN|LSTZJeAwL<)-mC{Vinj5r5ZPuHAf9K-*q$lMB;m46T5N^&3DDaO4NR z?Y>vUL)$o=%^K#hZHpLwEC61s@oNi*;R;>wwri)#d){^vIAH1Zx81?fN+j zxgHI_E&b!Qd3w?Rv^M!t)-U-YpLeUvKOf?2>ytDzRP_B@l3o3G z>XUKgdAEOZ$iP!yS$cELH15^C{PCaNTstkzAo;I`o+{rNU{$`@kXQLde?wz3Ft2i5 zqkRUTLp+DUt0@DzrMahy9;gl(c;jh{uEt6mPuRSJN0#n? z{?(WI2@ue*5x|u`|9U`A?L7b<;+Lj95R4z#lcx&GfwNXU{*sNBryqUlJs4UUz@3A7 zvS$FNgM0e?`@ub*L8Gudt;Ie+8Xg}XlKhlJ@HE8#en|4~AY$jCmSMrrNW&X7^*8)fF?1s2Bi}Ml~c@VJ$Y#I0q+!}05HX#0` z`?CdfuS|>;W^h{WTotjdpn6PMN`SL+mYgSQdZhh>1`W8hK|kXu@f0L4;q;*cMkFIg@G$_Q5rzsvZpY`NMkHglHjPty zqfsW!qRL4Rc5-%m+&Ms4oH>96s$Y#rChk_9nX0mms{6ABXrSn}V1m$INR(ly74Y_v z$>@)V;%i}N3_n^#Cy(ku)>7hwbw{-*R07!a9G3SxI*&R=$MlAnAhIY!mp^GF95CNs zG7{08>sO9UhHR3FQYf)UBx_bs`HziGb{HH4ho$sP za3-ba`{Vhr?K!SP_y6Kh>TXQWmisnh(-~1)C3c@Rd?F`GNA`Mm`ey|HvOs%(=UM}HwA33Xb$84H#k+8Xf_%;E??Ww z?HPQ}%EJ1Lv{_AB-C$U4k6$Gc!8LYpO}w^%Wia!c<@Xtr)JzV%q=E)Ys7gh9>?f`N zte1KLdEnWI^y4wfsE>uRtBk2s1JiR`t1+%wi%G5hE7gK9*aU%#6N4a-MH#yM<6}$^ z-WrpPDeEi*sCSAhVV{WiF}z1L3w0O8Q*qP9I&G;pgLd_+O5jG8xWt)7MgmiuOcGAS zl~p-%VJ0uxkqe#r3Fur3O!q->>gR5m>{HGbp0m>ztVv=giM6vc7hHO#!#)yB>OvNL zWMt+YtAPMYnKYd51QoNHdp9q&coA(mkK>T=u8DPoLpd>x6T7f|bxHTXhjh*$59uq- zH_@DBCkj!?@+ev<_8d_2!0fAxev;X=!~-SD7H=(rPqF;RvbQY>4pk9UU^FTy3_U?5 z)R;G+*uEr>r<=_Hz*IE*)=w-d=CsDU`6eU+MD|^80(g)rRz#Q@BA2db5B)0BN0sKx zWlNzQ1h8N5W=KM{KtSM{^gnfQhq)fj#X**v1BehpwS3}1uz za-J|X&eQ?!l`nGa0!oP9G>n?f3bmyjs|CiCQATW29%l$bhC2M*c`HGV*maesXb1c% z>7?${P%DP9S+zT=hbWhzLFG9+-8IL^Xrr`namdWp!9hSE561?Z5cwn8lJN`{XSO9D z?XO@tEtO`0S?k)8&kXG%XcuEU(?U8092mO%zm83|-dRj00|eA^vi>(<4pV(KI?J${ zvJ6*vld7!%ei`k~A3J}%kT=1x7$ za(u7a?F88+`JBX2(j~yA_$?-XYS^^P+2fKCS{Tn;4%tG}R{eeBlJ)`5y2cdiUuuqe zyM5hwhT>=agz?EI|NVI|w3&~7Z&!u^y*-g?h$q77y? zY*Ozvvmp+tXJ(_@pxdOki|NhMKH?9Z-^^r_%?)7*e~N#7>ts~)svq2*Y+ruc&W)}q zEj9-Qdyh1hEXGe<8kjzz^JF%+JlArzKCQG(j`;{u ztme63CJYOftAXV+g9-B@5=`V}CpBw`JX+YNU?px2CnS^Nzs3I63CY)^C;WtMl8Gbel)+jo=z;^Xt>O}|V*hlN|MoV?E<5&uB!EHo zgdv)TZk5Q21bhJ_&8*rcnGl`qx7{`wC1VG_j#5_0 znP9Yq50H4>hmx(L3;g3B!p^?HpZ4KotRMg3WauF7ld9yM#2N`%5BxA%w#$F-!w|Ad zSwC6^;u6;}1dY7n9+)ag-w~>t@ItKQ6~kPwi0<0^d%lk2^>;n5!Zf+Wrr0n#T_sA_ zs@QDA1(OA3#hn8@CgvYlu9FS6e`aPAAllpN!TD67^Q-)hQ~J(8T(dzwLT621pSxqS z#c-`PL?QAe(17d=s5kiscT9@SVhV#vek~vEl#Cuyi7a9h9Y$lA3iuE2l$;AH@82nz z7Ipf)cZLz?`ycF_Oe%M3op7xUTy!SE*yaH_LT7~qJKP=b3KLPXt~cF7p3wn4C_!bd zWh+vrEQiIIk+m$2%R|Rt-<{3PaJ3_fGiVG&genccnu^U$*-!ZnXZR0KPVD$Vgo7J2 zfFHmbasW-j=rKE;@}Eqhv1487=`b!axMO4t#MoWED0RTk@YZxXTJU_aM|L+t)AvZK?dP&X3$B*>&iQq)`WX z6g1kDG{u6+an5~Xmt<1(m_K8eWXiB^b7sZ5WP$5TOOZ}oiA!($^}AqYcl-XkCfn>6 zn7Sr6GfHb$!KzK^-$as@sij4a^V%3*SqfRYhSxN_PUQ8^*{eTl*JN17pH+z1yuikj z0vqoH9qofwr{F(fFD}h9g^t{TJ$~lQ5|Rbpyu>%{mW*jK^jis3 z$>iOVaXoRPU*0V_9cg`Yx8$4AO#k1zh$$G?ykQsF-v^-u=d>Y8x9t667`X)VYXW7VQ zzgmh-*)Q#!c+>gmdn8)|^e^{FboRJ-vIl82iAlRthC=4>#*qP~878?1DZDfAX1{Pd z1ke^SC26Tt*JFj(!=Rz_h?rq1l-w7kn>Q}@3>9O}QutD&03CNWxEpWRT zmJ}awISJr?p_N4_E-B6|>n!G1BE9gW`}Nc*y48p7 zkQrh_D`14(*I0;)`X4NJqknGiB%LG{WF7_YTOSwM1W-{kyxIXpp(6r;rttJ_yH7IO z7&n8es-!_})qo;wl%%oMq|*E;JzfUeYA;&>LE{^q{MpZJi50k>a%u&=C0sJ8 z$tg&jnewnWZc}l6H2@i>Gi?jP@bFo-kc5R1%K~4k`M-EZ9Ajh?F#-eOFBK&+jv;=blAT#xSLlSDFt@ubom4>YF0X&F^nYUH?4R~o#wNM-JgJA<|fJV^KjmoHp zis_b>a@mw*V*IZ2zn_w9IcRG=U z*y|@32|Ia-mu7{zKp4OarDVc3wq^=Z)dMO`=gWg)J1>~AD)!fg5U_ufFLh)}5X2gW zvWq4bqxqOl=ERi$&IU*Yu-968h0ww=-?I5;B{(Vs+@QVHX>b9=yBE669?Ef-1?A`- z@2Zv6Jfmj9B0(27lUd0q4A?|~6J4hhiReu!`d;H5_2D*+ZbIZ@)`yIqrOsEnHFs9i zc+=1^i>nhfgo@$iA>wjUEt-~A_he9?Oyw>zJme}@y3nFQY|9_6^bNme4F+YY`F)SN z<|cwZ>PM+4X3^k;Vl|md0Rn+&n#(K14;18(^#4cMo4`p?B<;gp)tS{j(=$T>b20;T z57&TF5z`C&dD(k8}C@Lx{u4sZHBA|dLAUcQ_2ns05C?0qq z;Ci6q3f?#0^F&r3gSzkc7k|@LS$RZ8Mn*H4|*0J1ml|X%p9iOie&hj5G}5$HRT5)gh`vSfRDV3HuEL zCxrciU*+Q^G~xj`i??iBnNW`JV#1hdcm`-WixzOa2XvuB52_m2qONR!1T37Ouqb(WFmx=uEghr|r=Q9YAn3|)@3bN$Q75N$n zq_z-K%+{f*oPnQN%CfMAL?Y88B!4?sKw7MLD|jd=R&v{_h1zpdMdosT%XT`jGg-;)?#maz)xlu z!x1L@)1j(o7&a}OjR!7-PG-0-3m}DKG0+kl`bXSmk1N3HWM_}%H5pNt%twZ2Hs%tx zCbC#=1Q#MtMlgzBAL+y#6^{HRO&~z-@$X00>dJwOL?6 zv}=#qI$V{)aYJ?*pk49&s_fu@@mQHsZr?T(hx2CZF5<0fQWVDF3vU9I?M?H zk4sGm5^A?nr0vVpbP4An`j^R&-xqwD3b`jN$~_eER9b{h3O_UwvzJ*#3yafyWYX{tz++9ZICPn8l;` zAIoU8>;X%n2iYKpR2icpexqrh4Yba{4>M>KnxYJ}L?6J^VVh1>;Y-$wxq=HN$)NA( z+IGoOf2vBh5t$hQ%(%~_JO^%B<`IfUHkm7@&%V9Kn1Oq?o@uK^-`f06TOnbW=7u*@92_mv z7-*H`S&?HWAW~d5V6Hq8hQl|_lSirvj#4M6 zb)Yvh<7=<3A_X~Hj_>6L1#V{NPu#z$>9wQO=}xarRu_)eN46IP?og^3{#(#`gWf`oA%JoXk6x--e^O_4+ysrSKGN70RTW1l4kzQd zPZ`-DM<^8UeT*6h;O{y{4Fue89iuw^@hvP%(gwT}nwRJC;h?dtD05s;GJ04r+XeF#wFMF%G4~#)4h|Eb;347r<5WK= zpm34&Z?%UtS&)YuuR360Pdi?9Iu(wEsBvpY5mJP=q4rwaHy*pr_9*R#DeW*#A=9Aa z{0LBRi#J6PNugB&2Wn$w1i=Y%wy>pU^ULw7vMm8YZHTNC^FI-5x}Tu-f;@{rK>+B| zNaFa-)<^<}QP@<(r5HCEKhEX0u``!ZCwu{zOx#LiN`{tURk4rY&D`mRZ)ql`02}!T zD}bgASnbF6kcb8@N!iElv>J=(Y*G4 z)zfeZSRv;qW*ebrga|8)$AOxdC!54zUpV!o&{jpoG6a~hhyWuG+h|`_!`Yp(e2i`h z$b>%RoHZ{E;VjCXbQXo$?wS%9FIMGH zA!bxvZY^C>n~h8h=L4w~^8ts;WM&A@I0`?;u_?5HA0-bsSLOo_DbW6OJB2W5z#5b= z4w@-vaJFG0@Gm%FrX88!l=smZe~>NuTWQ#GMU!DhTDK*?Vty_b@)HdX3af>O4YQ*u za2&FbYztD5@Sr>dw+tNO_kb(A$r$%L1&5xmc+8x6iaI6zpgnO286hP@=gEvLAL)`V z&h?z`GUDLO$d{K(iUAN3PN*Q|s8&Y9P9l7>+HD?LB8tsMF##-+YV>5{0ieU>Lq*3^d`;jWV6}cNkpjSZ)_Vm za_uvF7EbBZufVaX;+BuXSdSzjQ-#zh{_pIVpGF65tN%wQ1q*Nimi!nP+fiGTg9e_P zX(F45;S)qU`Mn5-FwBi0SDRg?K43r@dIkQf)7dF+yC*sF#=xdZxFSC7*=ecKb{dbi z55#0HqVczDWjRKSNvCIvt|#LQuSHOke9eV%#6V8SNX1u1m$3#Yjz}?~P1_!bpaq&} zmbIjtA*{@2DV$NWY+WcUnT5~Sg42+;vD#whGo9B&WvZJEl~8JkX4|8;6yVdCwK@2X zBE~HQTvu5HgqwpwBm|wLL=p3(T-#mZN_axaF*nA zr>nc*%P{H;^=|Rq;r#;`F1U&Q>3`Hm*qa`Frm8w~4!JdoKt==;rAz^%FZfI?wi@|~ z11!u>Lj}8^(=K2gv1BVQV)vf1(}k713&c9e^H$hH!x1jx`dhONT|G468sy zJlTt*;5D_z_~)vNysutc8S$Ss*Pjd1%v`hJTy?s;=xtLtTJ=DfNx#wR?kJ*9-u|mP ziqR)~pQrl7wz>X1+2F(iqe8GMgP?zoPgXYZZ)my~Ffj6jAftEXNP$T&I=?$uz^FUME z8`XQ7@83>S2RMsO|6A1Qy&eNOb4h1qaDpRpB0(Lg&7QAq4^Ph?Y;K;tMGbHD2sgFu zx!$6dwZ9c}3Kt#Rs;cmF9Ad|Q%Bgdkw z3>*rw6=WCy2Q%fzRvUJ10G`P9@fU2TmPF0m7rjNMU<2 zOaRk`FH)*Hct1?QA_T9bR86owOo&S}m8tf@jxYri0e(8E4#Cbap^yp5RL9_}FacU2 zJabaX;JYvZ*J!}ADU}L-3KNQ%&@)vVG=gXedkGVIr#c0b!UWvjQRntebq>;#!xSil zb?(4amtabm(3%PRr@97H!-O_W7?SE1ObZjBuhzK-r|N>~VFLWE5MP|?9?T3A%9t=B z)gzc4CX|CGgI$dY<{_az5~*z(He3D8DZnVWAT9sBrWJQ#+qHPBo2bH|@p7KEFPZP> zs(wXpzw}HbXyyOls38%v@@Z9Vj-3ZzvsLEed8&UBeui*q^mjqkMT=A^>)esGa5j$x z(F=mcX45=RomF8!_oc3j&elSxz+3}K9ey{qoeY_Jd&Huie>VsWUX@70D!j(bx?BBW zFpiO;HyQ}~W)d`9=X^;L{XvpYWgKi8KdP1&#rT8?73?zQ_oxwtvlw5^_nXYvdsG*9 z(og2*dtiWh!@PEny0Q0D4zCAt#TN^=Cyn->;;O_G3mrtv$yw>+SCb=Y`{f`@T28Mn7$ZTnxg;y69s$brB)L*^WIJq!YBM5TrAk zP*ML1Y<5eRAbw$W;tP62BRXH0cD4C#fvT6yFb9rFxB$rw)?2YF({{5nz=c~VK%&IX zma%T5<=OUJURKDjEAmD75RcYiJ5@HXT$-)ftW*V2h~T(scL@H)XmWA@%zX=0wT**f zwHyKX#@Rv)liIRS^$}PFKlqC70!v)rvA<4FY9z@%7?@@FO0O6z+MqH)s+A?EoQQB_ zD_~KqGtw1Gv3C+#5M1#WrR?hil;q(#YHqq$wRLVXci*c{>?j$5GQdA8#I`L1T=fRA zwZBiLaf{m>_o*)(UIU`SW(s-lhvK%K7-%NHjRgo7LI4lJ`-@k|#TZznMbFcLeEm$m z+F}UJ!gv#DpEEUTQ>Q{)qB%xVRj>mB5uyk16`6Q}T^jR)4l`25^LH|J5yp*+kLB64 z=eoQgMg_bF26{S(L`ts^A)Y>fQaZ*K!C>uYRO^4jYnB<%g=;xj3h(r7m3KBUWyuN{ zX;7G_R^uZjC8R+GV4{GX^hCP&maj4YN7w&>2+ZXIcGGNDM<=F;@oB~5>fxl9bG$b$QOnszd5EJ1BtpOjD6Y)!r$FS?mfL0n! zEsD^oZ0j_ma%`Aj&V5MCqjMfo$74PG;UU$&X4)@2=eBo(tABt~7zU&ls4FZeqH6Cw)JYNozFsIX*-iqJ}iOT&jTmqDI`In%hc(XM`nAaXw`#B$)R!dc#gfxQn(bE&Re{o_?vBJCxmVz!w-|*wk z1h0rsw;N33@-l9zN)GEmcFE4_I-R{byQrr)Axl#1?gjicwhi zJlbA|KJr_2cd1ed;7j@=h!-&?cop3;OCC|38=l0<1Ln-}L#`lwzT8g(Y9|uHb2y+_ zuv0*_mXa8CkDls|t*7!)*y>h1j775utFj3mg1)IEboPU53t$}p$pCm;K*I@FD&kEztVm}nD!1t?kk0(EO#YrWeLK=&e9PpI zGlobi;@SbyPR6A`8ve>*iDmfEqC^PlR;06q5KY_l-%Y}Jc^Mj}CA$2JOH zVbKQ5M1Fr+dW=^WsygN4iB$InKbPUv&~$!kn9?|Ch??&@3x&%QBPyD?p5} zqF{q-RL!szs?yx>v??u_Nitl(X?5SzYP7rPYg4fT!rWprXocFle3eS{vIi#c5M?JL#c&dcVbm1?l_is|%>8s=;?W1msCJFlAf zDm5HGN3T+UL4jqf#JhjzDpkGzqn;a2yO-fAp`Fo;V;jXT_}Wv#g3lyQ!UDU(@!zRJ z{#2$YJWx;L7ukwC zB>!Ingq4Qar4VkS4c;F0JG?pERZsl22QZIaGQ`p71El7#lQ+joqMIqFqt9;lDRl?O&q8}>>X=a)^k09!6<%`eg* zzkHHwt*C@w*ho=vwlv8}4Jm4s!2kGaVYg`|30p#;8MFIUl{!9zomI5lH-e>e7Lj)LMSWXHhv$C4;@TgU$*uQ{yiO#0F$ zjso8L^S7SCrJzY`~ zDGpH;Z}GvTUsr|c0;FZgVY98NkU$l&rUai5UCblVnT-HJg+7`~WyqGoPd>{ay25ZaI`U%S*xB*8M>3fE7u{5?=WUU(T=qOY)BUFV^j%9hH5C(0JN zB|V9^OS8kcZh2@fCsHjD2!!VRw^Zps%obRnY%*;?mK3PES>BcDj5{Pi^$Y@r>=9|G zHc!8WSORpxwS4(XU4+X{LK-7wVz}_lmtW33+l*ytw#dC=bP8D{$8QT)x33~$QJ9ih zhm;j?EDO%B`n1e`dSR!a<6qAL2Gk%pE@ z8kCZhm{3xfB#s*=N3%2QD)fy3n(|pNPxSoviiNp+Vk@q*C1vcna{l*{0@BB6TQq5l z3bEJ~)`GNJW?2Z5j2PMa|Bs4W_7#o5-uGK2&&{LtaN%oNRL0FN4_5F2>V)%I?h1=K zXrXc1r|;wsT8=e>Y!KFo-d%0(+H8lr#Yc!?{3xDonU&p=GP`cs;8o~2FLn=kS$Ik? zxuwksfSG17MvjOYH-%|6EcA;_!!V_pk-{IxSTdq8Ij|H#9l}+mY8hEn^Q*5po}2qB zJXM%2+u{xP0zz0e_ci@a{^u@PsHH{W`Fs+&(n1MoIG2kOe-6Um%O&s$1i!8;ovSPd z)UUIJD3nJlcCpvG$@o_&r z6(B4hjKELo`4IOGdQT0gXat)S*2A9~t7RmyV{+4b&>Pp7Cs74z-tr!Tb#}8iuq@%f z=n+yAwk2E`lK@FxPb{C8b#xDc;;YX;6hP3O{m~t$I62NK`E##QX=ZVb@1JXkX$f59 z3q%C~*2$`EZbogxT7W!)Ca7&!xn}&oReSSp)QzP%o<$U*GfGh2z}is=wStiMi;fyf zjGlZuyhfog?p0++Z9yw3E|6Rp8!{-IYUETg^ZCKCQd*;M*|CPbr16~+e9rW zkY!Akqa0CCg2TtGlqb)!g#fW41;k<)eiM_&qh3smVKxW`W{@Z(E0`nc_)x?RFX)QY z>$#Z^LW6+`3%MKs!M$K`Vv$j4X|TF$feZu4Y4iKoXuj5b`#yXHt}`7zP=Ak4(I^w~ zUoz7_P{+6Cv=jM-%%q6n_%*B5z@n)d6P9Pg1h^vf^wp|cn`x6^4u@R?d^OnKxRMpj zSdAkrZoXxkyT3X6S#^PXvo_bSRrRLeIdziz0Iof4@M_yETJj8}X&SJw-&BM0rRKDC zs#}K@G`fgz$Rh!SK7|HB_x|nL%v+~A?PD~t1s;iombR&7Rz{Tu@<#J&zdg73QMnRmaZO7Epwd!o0I<5t8Ih z{@_$8ZI(Z;dM4qqk_XPr8GBS)W!?%rR@?6dRli5(Jz)nz!pw_bP?bGy*NL;NxlwHbS*JozM%RdE*&p|N7$7)?6f{V=l%}z+X?ak*lp{v$@`3X zdOiF>zIM#@GrS&Yti8Y?4tZM(gc1c|r!kk2?!&+nMC1Sm>_&6QM(|jfh<}$(bdn*o zY(x@alk!XPZ4UW9PAXV}D(o z!~RN}aff?VH&5-<<^U`9yR@W4LrY4OmK12U&`~o?BvF`vD4+ii_7k@uW}J^y#lH7Q z>yQ}y`Pruh*{6lsr+c$cJf&RiT`Jifzm!bCA1r^znd?7NJu1hcU+lKbde~?A&aRsc zAE}W?7eyQw`ZJd^VkYQ3+V4Y!S`r512Sxi(ucnwIWwCGHhcZ1ynUrHYLfphlR52*JjwhA^VN1NB;!o-V|77VhKO6+ z9IppBUXZOgV68YD_ag!G(tjj04uU%RAbB}PiEtuY3A3?B_=Hz6A)=4ylFfyFAsKE5 znT~<3GBkh>CkaJC6As)XGs%4tiH9A>y(~9)cE}xSdTu!6ZUZ#Ej1WNCFALP;Vuoml znu(vNwx>RX5zMa$2;^!LkI;1Hc}m1dz`n?iG@hiq^k;E4ZAtcNIWS`f1r3^CK2d`z zL7%iyqGMKBA??D>`LG%JsjBKaUnc}fVZZ%x_ROt3Gz?B8UNkp;3a`6rbN{ERGQBi} zIgrT^hZj~`Xxo^C`pD`Mr6W$1YHI$!0EoP--Kzm2bM8{|VTO* zu-G_Mh3oCiO`oYcI6N-@42$JsX4_|KnzP(o_#gFGch=0N5B{UdTzuX8L4CYAdY39k z_mac$a?FI7FVZ2eNb zfL)6Fzf%2B&3j*APj;C(^=mmV>*}x7Rq*yJ`9>Y$tT)GegF~I3HWz=RMpu2;xH2*t zvRUBb*wF9}FkTe)o%#72)vfwzjSe?9u8trMCYX?NAzLB}*8$aRH3xpHI+i?h-AY~= z7xYX;gV|He*l*Pd&YY%azs25)vlT~%tNpRxlK5WrZ2OExUW~;*%9e#RZ z%jQ{A{1cepbEfA{h>>~D%FDHmbeDc!){4)SP(JzLhtgCOEpmh9QQ!jByO|Exy^)9tfD-#*3Dq6VyHB1FsnP2_0Tz3V&9IM$dK~<29Npore26k;zV}VXM7=S5-dN?}% zdZC_$)7Abrp?jp^W5hAK4|6pch4E+rXJ#}=a!cllO1CN<<+j%%cu&dxja9^&m7OZ~ zF9E9*z=%G|W(dQ=UPdz7-{mz05U_gU4PZjf!h5&T{jsBja}OZiA{-P<{&~SP?zaQy zXq*6>O`uW*nD~M0Y~HAA3l70isBlQ@?oNg%h;w!!QlJL7IDr+ivjrB3Q=QSWfe4!5 zNgghw8@@+F8mR+~1f?n1c|gVv9MOQpM3i$Lgbu5KP>uy=lo|O8jtun7*k4rN-lqWY zg8uF?fzps;`nx9}>sl0`KbniZ_tE}JOcpxUfqEd~nC4$p8Tn} z5WKBJZv_OGhwyV(KUvMLsbq~w?N%L61wj4_IvG0z(R8}h7?h5!t@Nj2 zn;2LDiW=dOuWO40MM3G9R5`FwIvReGekzWM>?LDd9gDE{yJ_w=C7&9Cgb(`VY}GQao95 zB0a9HQC14J895f@``wbd^xnqxn7!Uo$xdQ#& zv+z^W-#wki8B8hoAZO;B%qisJDs?)L8{yb8R{wR%om_S6(~)j=*~RK2gOOp{B6pll zl9hEA-$(^Z0FW%e7>@4vhGp5V(=HFawEP~>@%UM+v9~y`PCHy<^E_BKJ0wmYBgZ>8Hu zp{$tXP>kR-*GG=lKDhiQ^V5F1gBkDZ{Y=nWpH(pr!406U=$K@rx;7F-4n^-H!>Y|+ z+UTpDx6Nm5^h*3Z)K;I+3iWk@+#ST}qK690OZ9Q6aQxL?2XkGiKG@k}noIQ{kZ7Vz zUyq*|Wx6*KpD)vU5yWrHbYDab^eoqBKrXzYTzA2hQcKJAP{=wzmg|R0;Tpp=0AjC| zh@exCGtX4$rpga>El8@rI|Y=(IRS@7(1rhzxv5eQX5BAU>Ju}t46V{1IiH%n+v%F{ zQlZhIG>xnYxD&OvYA_>A921mC$`sD;%7^H zWO>tE+5!ChO><`lJ<4e^@s4_zyZ#aLV@F-(SIguiz>pP!1y`8PNj<>*UYk>sx?PnM zNgQb{eaX<_iNHr-;GLS(Wi?OA(Z*zx9BLZ(o&p2oyMMV^nbdo`>#s1olDeK1CR6yG zoO{14r7K$h9BQ0Y!XaeAXI&yXNbu}`d)K&Lwm{8{W2dWGi>!NkSF z=R=>sT8W)*ni?a)tVy);kq<=rn=R$Bs;+Tb|XtAvNY_idqO+?+unL~ z{Bj0(lp%53-nw@j#%efq1H`Ic`ndQ5tg|b=UDQjTo}BuB0sL(*eXjGZIit7kTmMoX zx*<@(5SBH8t{3-fb{veEU21Qh=?ww&4f9EF-9J8)jkZRkb$#^l7}v%=SRv<_d42Rb zfVpTNePyQ?Ah*`J>s_ol_+5(&7p<(ot!Dar7W-G4rTge^-KSlZ8*~>e5he+dnDEps z83}e_07x5iuQY03u(hjAzkT(YRU{%-49krrp}scT8nbv`UE4iND?SBs(hzvC&=7>L zC`^M;bb&_flK!->uEJH^<$ZNO=QA^+FJ{%9SDU}})m1e=W+4-S6D%SU^t8>aH1qmG z=6%v^Kt^byANSSE-D^Da&Kfn?Z0)Bn;ER~*ujsE2w|t>9nc~;|b4=m50eX1rDKH<2 zfiL7FH=0EQ^fm4cVO<43#gLNgC z*nWd`XY}IFgY`sMFt!fX!;-gUbKMRNE<&vZL+emfcL2~#Gi-m|u>oLmZ!ZG%YqUQb z8ah2Ml5`k>^$tw5y*SFynukIdPJdRm+B?0(W7=`e!u|ERnoRZ~Q~C$JZ_y6%!w`eo zeDkM2=zY?BW*~6xAICgW3&n}nsc7Jh2?~dc$O*WpNj*T>zBeLOoNKpoL#>c?SDwM#rx1*LMmbc zvb3zI!2IhEy3eUF3=U?cxYf?1SDN%}u(e!cMIv1wOKH~JJeajg+H8+r6`4ou!QnUz zPv$Y~+87kxn6~Bs-5pbN*8!LSea!v`>VbdcnAC7gV5Vg4H3UpZ0e{fLL_I$WxDm3s z>oFt@<(iCNOK1=fmSDVCL21)72ZAWD?jIarsh?x!1iHU7&%7Dv%MpNZN`w9@5}$9- ze=e~eywFPNVE&Qme~=!BQ0(~!>AzQgAR;a#FZw*Vi~~#JxE+7H>2t8ID4pjCN1#8G z8?5Fx*f&o(Sda8Sk#Z3bkKp%x^Tfe=h;xtm`Czcrd(6;7L@b(ki0-UD)`_FdP1kv) z=E*~_(#|!n9iqF|$Q+_PdNVt@0}nI!0LL9%)bPVlhk<^3^GdAX1Px12#%gp2J1rVH1+7CW z&o8rzWpFYdTcMF4vPTh=z}9N6L==!=y8!@k4vNzjR&R9S0h1mEw(y{NaG37<2k8)a z9XC3`JfUreWqezP25XcdI+zQ~v3-KTf*IOzIHD4!6jG*-rxYZBF~cF`RGa0)b^A8& zL;4{9g!30S6{A51^YL&+lIT2&v z-z+6^@6*iA5qhO_uUT@0KCjKBLTb1Wzi0}sjR$L=H~aii58nsrSR)xw#c-db9>YCh zS}!^7=oRHj$G@`B9pYkbs*jXn1L@zY!mR$I{^My^6$*C=YH{sFQCqTwcOra*&#nuZ z08jAIg^bN7vLWnB<4G7%A)m-~f?_^_MYz~G#}hWUqGH;_IyU}DEMQle`A6!>&ScYj zq~6;{ljI2i3(+BtKiQl&Qg<)Or6TQWbN5Kh!mCXAQNYF;GvFwgaIP}5j)JPN=GLQi zL;QUYT9Jcryu*&h60phq?Pxu`Y^#GFVy@O%jcU|<2{fj4EjF#k!XfRfV{|FL{^J-_I>*d92J|<_ym$} zp#T>j3wd^qS#IAxJ60r(*2n4k&g3Ui=#bd@4iIMmHL=BByLSN&UNFhCb!oewvG72& zO+`Smi9{DFQz}?1Qu@8e>#?;zVI`7OhhCcKjL~qZBV!eT7q4dE3Hta{qnEJ+B6f_C zT`L%F1uGh5PClDOC+MzaEwXWH%BN592_0#3qV8Hj7X0fqbBa0gL|sWS;oKATShy3s zexm+Urxvxr;sI$j6V=3KB{XWpNxH|co0U)3odkV&j%hjx>%aqM+ex}78)$Vh8dzd3 zIa&V+@_+Nm`d<9}^AvrzX`m#q)*N>>=Gi)P$=Mi-g=Xg2dhEFiy_7?W#OeTvD$DJ1 zybET_7zuTc3^_)H40egjuOZ<-?{Q;6%aVtsVV40MPo6U!@zA%@td0vPBW^>6{ zEPCtANBvNCp?Q5Qj5iC7_gDQw8KxUmYmpy>pWsxE=go(Ig-U*vsXh-#zS0Ee>61Ax z(Jw|&qt&a;z31t(FkY?xrZ0-Yw9k{jO`p9~rMdfWdImOo4jiZdf<3Ui$LZ;i$R2tu zT5raT*RAm7pW|Vgm}_1h56ztu&-`ZsR78Zd{X=(+vjZS(crW~~E;D~b5&$ipsQYM6Aqq0E);}IwL&=me33q^1)Q7ABNxG*)3VPyp_=W{=PQ4+ z&+W|T6QEZAKVkf>JdCFX=i}@xk5f);-lR9^$B9`{M;ydq+_2kC^vn{07XvuAQO5N7 zMu`71w1WW2u!$Jecg^yN`e-I%RI6TsBy3G;p`W=BJOWzH$h7W~SZHM~DCr5a@(;1L z=H|36^%iBvy*j}?HM{yeXf~#S>#NMyY5mt06!6zZ-L4BcD+WKmQ}6z&Q|8o5p-Bm4 zkR(AB1Rn%J@M|su_}fSZqmal-&FvRU_D3$3-oA6O?prW5p90#ofbW2Rg6ys^=l&C% zV3L{nPu;c48ceAG){%fgc)|VGtmJtFyCblcLe8R@6q?mG^U*)`zV6IxOwA?wz_K?W zKj0Kqu6&u38ejt$cZsfY@4MDqcZnYAE}CpMUILINnjbC!4LoGZFV(}k{RRfMSqwlP z0=KlodW>u#Zmnua9tC9?C-jItm#2Npfi<#{OK`0;B265OHo%IxgD8up6biE=1-c(d#lj-o zupuRzjRX=(A?3(OL&^-hQg?DUJ2$UgKFAdeGyCIoiHIUJz8dS2(zyrFBmuUd=-QBNX<0!%-wETEO zjb!EyN4CxXgl*QjXtT0qn>MmNY*Stu+UMFNC1tC54an>s-RU$`A0@|2)e&w=2RrQ` zv3tuzpFuO3EnWz#cbj{!(t{-aQQNYR>9eTj@j>i2Sz62f?e&}e>nQ!>l4P+fxKEmq zSA%Fbn~San>ufeNuhzpTC46|b-nZJq4WnB_HT8D8BVCBy7Kl52C+q95{H&Oa#cGb} zat&6|x#o~-bYJ&J&;0WmeM0(tiA#auKFh4E%eT{j9Y7K-q8o}M4VZi`#b&%#mAD|u z1izCqAQO`=U&VzI#2U>vWa5@djO3K`kmw zhAy8e^PYKlioP_g3qpL0^a&N@&0!&GB)0v}J@fMQdRACS$lby#0JIO&g|hR~E6WRV zl?lB{XPV5rH|V)x86mCwGRG%!Wk^cs{W`c%P-f;V!oHmj!6SJIUJ2&T2uJ6+Vzrvk zz8RCcKH?V_WPIuP+PWl)yo{&xcY3fq($5DT0tmFh5>o(mSya7jM?lbFCx(*BxJQiR zh~>nb?$HSV5sjr~64=(_#f6!cxTy{lDZq;zO95pPu#km6-J$~k zV7ikk4B#zcCuR#=qVJY{;^t=<11?0zah~#p-0>G|1h`{}x(1G_6I#NW62eF3h-~ji_Kq}@q{c2GGqa;LO{MiScCR zFJipeqLu}rHhhKLgY<>0K?PIiV?!zAidTLMu*2|yCYljO9r1eU~3QDdVE z&!1M&Td-IUbgJFo`L|WCktF025sJ9TV5i*%k=NkR;|2+a`&l6PP@>*$>Y2ov11W3ChWVxtj9~WUS^56_nVxve>4a1UB)4=Y@@cnShPd;)W&5NSqP1iNVe= ztULvSB1!zjxhEV+7SqHSNqV#oMSxeAN$*oTa$^wkrW#2 z1Ta~5Bw2`X0FLu)*p|%sw?LfVY;L>-29xdP-?zZlz1;-2>h{hP=CoU(U~e~<+^R2x zvhwAv&{FOriAnS)Zq{LXmdeT=x<88H7!*&@j&mV0U%t|AEMvnU z3>h2JgE%LO*y~#b0(4@#K#HLo3nahx5H2ovAo__jW>%{eUI_5-ydAy;TTGXk7?)LM z=uBM?|Az}^>Pw(Y?V1U7bED~d2kb_x&FOdOCUn1YmL34%cH}JG(q9)G4N$K#H_n39 zaRz_q9V4x=}#f@t2!}?gYhLVa%O+Ozl;bZXy&+RYor5Aw3Lq+v2$3 zekM6vmsj|>N7uzByW@X~35#Q_Wi@xr5wl?l+id=Kw(gpGS$tL|Q;QX@2kz&lWK~0r zD7n!*F&kb%8DXZ;Esn5(%s;Y?iEfD^dld?#qh^M=3@f(G9(jgS!Vu=uf?3-=d|}d% zL7a${Q6)m`J%@FFuIP>$Gh^=peI?9|cj=mRizUWua8o6OhPm++)*;g8&%&ns+LDIL zu0WtT`8-dH5o>r0L5N}!c%ZE$3kdr0i{LGSrS!7#2q0$-b$j6yO!TCrA$k;-d89P#F0FL-aX?HRp_znvMs<=^&u8Rzf^pEU@!^Z;+ zxcMC`hMd6#>zV~8hGV%fRREJ}h{8Qm*1EM+wZpd8I>o9%rAo|16>ED0u)NNx)g^_D zld+nQ=ro)1#{C45QM1sVNN7c{YnGAy99Zk+G^<+A)X&wY4)X>(K$swJfg0dki|0`1 zvBb$EWuPu1a$u1bISpPGI9(aBl+p3E1?;)3+Q*pcdvwiE5HJQ4OHX6tCF7F?e}}A0 z-!;*s!b1&&vPKol<}JzQT&#NlQTky)++v2$)7^()M-OPQP@JBH0{-)uAjLA6v1Bn2 z$Hn+8$OvImbYqagk1k9)qql?4( z>1~%8@TpF5JX8Noj>S!0ueO@=@6pE|K|jF^ZYDd~ZY{ADiV(%==T5`I4n~+wt zRl_WzCYm1eb$uEh=GiKz=c)w0$rr*=hw32c%CC?3pGjfOL%qU=Xl=^$XMt0krNxC| zfASd3mJnmbX8U}dN@JPM0x`P<5SVa#4*EPc8bhNpo0sJ^x(vqu3=$s;8x{T+5?H2D z!4VF?5kV8ZG~dnFtPDL#@?$TUn5ne|lN3o57!#>xqQKA$*Leiam~jhr z$78vIHipSr5@dW!hF+j_A~Wf5Q%HxvwG_Z~Ll=ZDRf7k{M+LbKYHq@m-J@$e*a-=) zE8X}Kiy;{O?r8r=1gt7ydfuz+(l`#Cd=^4CT)?oS+uvQw-;x2*75oK)zC)nM4n9Mw z{1(4~9sbo|^PHs&Bfvp$IJ)csX@-ZGm2!+`<6(}rLQ0H6ii{kN^uxDzz|)2R*|S)&+#HRK9tb=viciaM@@pUIpd2>}+T#_Twh}z^1VO@q>dmqD z>C^$}0Od@qkr-PS2e?|(BIMux?ueww#*~yHUyQ;Jy14+?v(hjktAJT_pYEQ%A_^%^ ztY=%Z#jSq+-%^o$Aku?T}qDCfE41~F#NE%9}xCmqv`XB<@ zWiv_jHMH}k@c}~MeEY@6nCk|r^W(07@}lQw_TMZ-KVcRE9y?h^(wJ{);~0xG(U&ai z?9Bn=(a~I_M3k}Vb(ls_`S`6T+~bQJ(KK!CkIP_>C?-ViF>vS1!U48*ils(0Dp1k3 zgMmu|AC>sQ_)+bEYC#P{T*;w}B%KE)K@qW1xTqa2wx#U|!aBNEWd(pOi;BMt{>z%Q=lS?)uxXe6qXlY@g6?f|A%0xB@MFdaf_ z{(U1YP;(v|`CTh5)n&0z2~@Dsx(zb2@oFNKNBvO^;obn`_!d^$=B&2R=!RV*cQX?o z(*3It+@gdpNVRrTf)2Gw)BKPwE$5~N@J?!%m>_Ef$I-EHN8D_VUL?lse=O4b;qBo? za75T>K3b#?qUN>I=yJ2+c^x;0Eru__7IWTWxKO`p9$pMV^=0$=Vtur`#W8mwq|6+! z1m17W=J+N0Q1=^cW-rl8_5HQ!g-h?b)ncYnQ zWoJ|OW%?b*`NDY1u?hH`IdM63*XPVd%k_zc4`Rq@gYv&HZ!On1q3GDhQFM;E<#9bL z#n6nxWGi6WyW9S^Py z>K(_o{t~|ex*kQg9lQ~ND-ObT#Gq!Q1wRW{^PG`Y0_O^Wba%)>@B`@$XKhAOLE$IC zPGA&Bp=F^E-m<)wf|i)D8AL+nOr|gjDg%z()YW^>l+Tc@{Lh4I%*^cJkQc(=)#tr% zh`qvbn4lW-)zf-VdMVJ3w4$;7-6yc|Z-eF#o^SWu;AVuUrKoYG%&7Yz4Rb~>5RX@t z@{>BnUSJd^f-y}vw3tgxcgj-;Bue7yF>QM>FWU=^&>{3L3OhKEnTF^xIz@v)!dj9Y+&V}Z6tiKl~(2bU^D0porFKUIRYAf7B@ zARTZ2LLgn6WEBGG09YYHcMuw5t%xp#;3CswC3NsN%?T?p0pB!ttihD}nxC<;aX>Q)5spl3 zoCV3mR>?FPyh?XMj#ijv+*>@NSeY@a5b@RycJ#x+T#69!rp#@t5b;*Y_HjoZuDbvd zcv;4_Rq)_{)AU@8NQ^n=iq-mn4sVK`AMRp|c&n(ctqy~YJPty&*|J&>ky%rgEajq5 zhM>Q8rq{E&;!Mu3R`i=~l?k^2z8Tnt$iBewcZh(I94*d{r1<+uHW;d8EoQ+2k_$!p zD4hHLEzy92Fp-I14;9n;Wb^E^`oeCb&;>YzjR}g*^Ka0&O0UqrUP$VGr&eO@WEosw z&U{W^2_EqAa~PgEreY1IYmFJQ22L$CX51RxNjL&dF2}%=BTPZ-1=2UVE|MdBY?iLk ze>t@#k*pDAt0-9mdnU#_mNA23p+vADZ%2DhE;6P|Xg86S7=eVTO*7tD{9sWWl`QV% zT#HDk;h-x@tp+Ng8zZ^p~iN#G9}s|Tmb1CN_25T5nXmnZPKP$~w$D%* zLJkL7!7|)G6GkEeC9SLwpegQv9%-}#V8eKpas)=Xq-Wvf*kVsl+6sG>d&78W|Ife! z!-b{)khjg9&%+rmZ^a&fsG-;ku%>M>C%*uTYm=Gqf}V%B_UrNXvKh2qpNY3wU_0=2 zd3rq@L>@D5uGb5Yh*R*|n>$~Gvsbfu>P0v+Er811sOxOT7d@R~` zZ-5c8(QMv;AfHClsu`@V(HzsP{|<3rO*7&pR+wHd>vpv$4_;aT%5W~gZGZ)ssD&BE zg-5RcV_t^CN3)s!vaJ6NuV8jInsKjSai3#udqwk>>PKGDwWW<>Sdx85#NJlh4t)9w zgduFvZxqR3#714GS9l3rc=(TvK+021(>LnY4*GQOtNKiOYRh1NtUTPaRQ(z_<{We6 zYq~uCmzNY&Qq8I=aW&%LrHey)_7U(f(_dNdzLGwJBfH)ccankTw%$BFk z+pp<6YRLLD*SOk3F#6Szu?$)E_<*?2X|Ib6^5pBfeLHwrmZ2z5F0U58$h^o0Ui{VT z`s`wSM^I3*+9sXz1~4_pOnU=fKXc5}Z$MOIOAL;Otp_Y?9&S)cpzQds;02f11Sf_$ z#%$94+D;XCds^APE)^$+wrqlr-5fLFO_+P1GV|ZWYBSYrcvBx-jouZ5g*{HSratnp zgl*d;`+BqYTY4l$EB%%p(QdKSx|Rz!^wJW@|CUbRV)4OS2-#a~&VO592EzK{ZM-cp zjqkv&x5rXz9(o6XXdr`k-$4+B9b>FD5F*W=H*5HRU?7rm-+(W4R%1@v3}=o;GifuN zr(QDOY=*<&bLNzH?ed5VZQk3YCrD0u(lynOpP(prC9k zrdFfrzg1s~x20Q!mfqf~JLu;?ORLN;{Jy%W<2L=AgR}HMd`}N;3xc6H;NVaZ#@Ih= z4*a)%t?jjflp>4jc&d;UbbDVv4U_su@1ve;P5cAhq3RjQ_LVT_9oXN-t56VX`9RMu zwG;%i3ykQkMsrmG<$U=8++jfyt?=$KgcKif|0WLqZrbNVeS{NIg9q7w)UX{QZ84}} z+DCeHHQH;9N>5WZI+qL5yv_zto;8`h3 zoKPxaCCI#UX^UV9oh&La1WSZfaE?*x?!$)_$_)Pz{xe(6xj*XO)plF4FLh4}bpvam z>HX&%691PB*q~- z;C9|+D>r_S6l!m5Gi367i}j_Z3o8? zAR(kfJdzH7_)O?<@BauLZu*bVVf1rh2|YgtZ^so9pXaz8Pg49gk?oe%y~qkhxZawbKjmwE8@_-0H5gfw>H3ZC?C#cP#5ek&cIbi+j=0oP z{XeZR@)Kxtv037WTms6-+IA|wvTX7~#(!xUVEEdcPfLBVJS#7y&z z=!`v^CS!g>i`}Ei6f|G!L}ra*KGp_c42SuE`~1cREo@{W3up|1lT&a7i6Qi%`4W;~Hi0o;F}^{0jRfl> zI88F1ecgzUDpOg~8x%R&?!v-Sa5ByN5!wt+rj0#(!sv!ivKua6g-`Q|1GGYdNjfu3 zB#o>%@3XntUPA_C;B-e^!%)jO5%=)w#9%YN!0T!*EA$GmVjWQ6RWOH>6p#N%cf`|O zYyMK;^{<euv$b%r)%paT@@ND!zvig87g*$B7aCaO- zHpLXhyL z0WJvS`uD~tq2XY!7>>3ZT~uOLJ6?U-R*Ln96HO}RRB*r{fdb41shksuM@BH$bZNxJ z)ME>of>hjX9a1^Atq}$R=Vb^&0uOStan6akw*4Z>f*q{?k|$wqaJ^LTJ8e_`Y*vDb z!LB2y*!AzSwE|dwj?^lR7n&`u*SGJi5HScbxIIJ+9%ov_aAfWXOCxobg{iL%^3HcE%|_+bnOWcAP_o_1^WAuXX|27o`gt+VK4@NaCCd>%Zmx-k zGqQ;O%h<6%gJzcYD$Ed^mDs~3JrJwiqm-9liZL6vnLD&sX|B^=aUuQGQB|csor$Tv zY$f;ns*-4aCHYO`_{Y|62992y*Wj}-xg*RitoQ*l(erx4`mn_FhB_ac7YcD2Ov{s= z6A63PGiDt&mjEF6vc!&K#X%(?JMwS|A;&%uQ{uwvxoM9V1w7z0_P%p37!s*!9t(19 z&1U2BYF}6=YQ`tLR5{nzYz{85j4yx5+@A2-?SHLsuMyyS?hyVXCy4;ba-gKU$AOZt zj`9i^mm6e=VK%sSA=Q=VsAKMatg`E=T`%pj0anQlO*g}eyn64aD^>#Vo5!`Q9@;pG zk064g(ZM7<`!8{#+KsTkf|(*#A>%tHtC3t_6m;a-z^*+T7y{W6Gxa$&$Xh{)t+%;x zu-BpL9X62&KH?88h^T%Co^g@X2SwgK?KezEI^{N`|1?FKmFdq=%I&6avDe3aV68c) z*sCx57okIdVG=*wvcnG=T!>F;Hpt5u@TX30LK-p#`**VT?^wx({U3pU3=+(Yz<_w; z+t1%EWBb4RdD)Zr>}NiQMn3$gu=G-V_WV>>`>(!#^+H&?xv<0=;(TJ7u!_XLx&f&Q z=Y%wO^-`u|XP5$>Fs)m8wa{`$cE-;e=AAB>t?5=?MdC>y$H$g?#DB`%)ygx_MEmrC zb>lMJgJ-Vsy(gVp%%QElVP!NrG3w>p#W!~ThWCQ_l`v#JHE$@DWxF2>(qWPd0V8@tSxP|`o0?705Wt|O}rFE#6 zGt9g)?`%|;DE9_d-o|Z2TN(Z)A!I{uQob=Kmji-Z&2{DA6LZWf<=*KSo_#9-$aHgJ zg?B3UOqwcWEZ(Z{4)ib#Mg9)cvC`|(>657JUO`UrpN>Hh&ORi99nC@zPndHmy-t}z zQ!Bmh-U`T{he+r|wRx)&qj96@R^_cLy)jB1st_UDP{6DSdX7=;fbtug2DI~Vfz6F3 zsPSgR!>XS$@6~wc;*6CM?Y)El^qCMoxP<>7>z@sUt`;IVw)FkaEb%N2t~ z*Q3J)pyiPrQ3~hnbo4qq3(U-pUMG<8ijH0bDcG|V{ER71dR@vw{9zZv{}iaikl?`_ z3et$L-o1j;@d@*{q}T484V>{WFbORl|*&9GjUV z-l7h8IcMrHJCj~V_le!6b;_#*)ICz({)c9vCZ&O;LXf~R35HB)KhA(H4-}hl0o zKm&im-2}9E9gLqz-QnGM~%c1NWyt##&u#0pQsi8$63 z6!+qZkN^SjIh3PVg+E39+L**1BiSBZO$l+Gr1>*#{3;T9nZx?3}ma!UmdY>A#;5e_$qC?oj!=vTc1% zy-3=>F`IUuoVo4aWA^Uu^>dy#XLa}Xf|LH#?wG_6n5ORDgni~i3&K9@LN1%&+phm9 zo&X)5-d1F)QLNdJOmKoVbK?Fq8eA_)Q!Os4qU-?S>Q+ibl_K$nf>VWk!RMk-NkESB zFuTZNWxth!USwIBi3f#XTB44))FDms$PnZ(r|jhwz)*l%8BdliBcWULx#briM8K2w z=*o+1Eg_QaQe>aO?BYL;7sD4ir)F!VAL*~k79@`N4fr5WCsCYElfQzU_Qh$3wJPe8{UA37h9x$uqR!b5;VoWL;$v0R$$o(B zg$8TG7hHjoc8XHjGI9}0SlK74D4G4h+fZbTM3gP+nEfD^K;@e6ko_QcrQ~|jKKp?; zrsP$X#{PTQdAUI)S66lRgWMz-et0NWgl)gDgxn_>egM26^YBBfmW_bq!w-Jmhi_F; z1!8u>FRk;we4{uA_#tS6K0F5@ z1>T&f3=y_?s-eY;29`DE@!sCxf_3Dr@N`^mcK7y@ZLkEz8VZg}#u|97QmZQ(3Lxrx zCs+Iuqu2q}CiF>wK_lw#j+wcAyoK&BG4scLyoo*5Q;9-dyYb6dB5IyAfYe7~%nw-! zE@&`*;~bgq_wfe&R%5&W<&~!!#g$w(5nACS+#j5*D(Vh#VPj+8BA|=>iy@*BESQju z^8Z1X-1B0H0yi2*;4(%XO0D9>5HV4+xln``L#Xp`>#<#%GS@=j47|*(5YePG51*L) zTOr~l8vi+>1dDMNPA%cB5U@Yk6*TjJcI`PQqOU)qjngyM)^zOa)mFWN%XDC^6HW9T zJVOD-3miesQ73p+*uP7i>2)l}VHdQvKrE8`w~z%WU1t{d^?0<&v#1D1acu94)pVgL z?FT_-ojJIlw>OW-iHMzzQGN6ck#kiSnrr%bJz@|8KrJ_!<^8;#C=i~PQ`q0@j#~Qm zhfcE4oYddzhHsbSZLmFs1eAnDErbU5oZ!7QSo$J*QN`y<;G#8xz>0)0X%cudmU#E( z6;nf+dsoR^k2#~!xG|w`wqaZGA|7D3ftCV z64_%5vzKZZe*n&{^9Op~$j!$pXDs*K%aZJ33^h-85yKL_Y0=Had@s<}VR!n$sf%?|XAFb_qgyq5mWYW~08-ULpn z;#&Ogdwb^Abocadfngn%?g3>T7-Sg)6z(Vr?z?DQAd0dHh$K(5H7c4xQO7_iR8&wD zP>h&h1|upc>Zn99F2Oa37>W9xm|(<6;_rK^?(J!i(Y$>A?+>4zyHwp;&N+4J)TvYF z$bKg^)h)7pk}+72<;?|6Sr!_|=${Ysy0nANe z0>U}0;`|jZZjx$;d;L4?rBSHF>@vwV!oYs3zHYeJtwW@e>X zEW4gL%sbpgb>|)K^=o-;SfjXbqBY6mmeVBo+T=B#WI*`Y$8BuJ}MqjPeHY^6*jK z;L1$^$GFTBAB+SjkOe%-l0aM-& zSZ0r?1>sam@n-J%RML`#a}TAOlrNX-@ke^?i@rufATtanZ<~+_29bG~Wn?(WOtn-d zjYMHNA*iB0u96HPsMc~Xcnv`nW!ZFqECuZK9N{Z3NuDy>$!N;h4^;0QAj^KB`naGb zI~(g;$9P#Bx>(e&7HKRIJS-Ov?y_EPHrpqI?A^k9SDBF}WIxt!QADSK3_r3Z{kO;$ zUhp@$l<OpRelwG=giq6Cac>w$l%9J3Ipvls3L8T*JV^|&ia6>c_| z8!~Xe-X*$dH2jM6q9eWj6}OsdDK3=9%*L$XYuBxM+mT*}!MBkCxHe?~CkCN6NXkA! zNhGe^dYkSy8kzJqJ!Z7mxwquAzLg_6O|WbPsD7bX_~ISp*>H!xX|y+@_Z)d>5!@sf zQ5a+xeR98A{s=@FOKZ4?bALYCJ3VLloP8h35q7j&2(K4D7|y1VEMiBxP2Vc%p`aLv zGlj!^ucU|bek2iD+BM(j!l}$QAd(#{<=P7DlxWi7oOR_c{ks;4# zB8kD$tE87Zft|5!0ajkn=N#>IO0R!8G=PZ(b6(ch9POPo{t(yhsV7esu$XuX+-Evbnx=iL(`lB&k)jnHvTSI0~J%&cvq8saVbN48H z)>toa`smxndKE{oA%uZbthSGJl_%#h#o_ZVM|PQHk%)pTzSON{!JDd0jLW2=1#kRh zGr&Cm*@iF2dL13S5|$t99p3p4_?l7MN)I8~P!$_P6`OiM&o~x{bXo4HVU?!L_=-&Y zf2+sUCogB|ru6?D>vg)QXS8g@S|C)sau){YcaauX_ExeFDq+=Yr)?n1>Y zcUJKV%KR(!rN?=#+r5SKB0dEn5dW%@NKt0== zU)tfcjk~o)de^;Pr$e<6S-1eY~y97@;fm$upcTdi+t&0FGduILR5I>Yw5L7sdLsXPxW$ zWWv!-4?a2hI;X9kb293HRSRBzS{D8fO?E~}!gEeH-ktYM2Og@YIDK*HdgUoz$F|{4 z^>dsXLfHd3an{YpEzK#2yua3;z3NQnoXE+idbJ(Goc|DgGUgf_e1$=vf_ZkqmS-!R z^$R|E_H5+tuctawoHhE!)7TMMrEi|*bmlbZgVUTz&YA_|pBv%4zTo!fe&`#=!@_hZ z0E&80e|@?))?NIJ0b2WS0&VTnFq(t(&1b?ic0O-%wf<(V&3eF@UQc3XWY6=u9su-T zy`VQ-==E@4eXcp=tbI;zIG-GQo@%ZoYo4Md?QBa{+FS>yY4el%!Lumm?M=4egnBejPGq|uC}EO`sMF=Lv-!^PHU{A zvlY%U(EPId9U;CGJ3E;~-BKAp9wheJ{Z2*8$PG438}1PIU)P;iJIBdh;0$lLyUCK) z0T8{{1cljg2#%C-9U&B5^1&6egDHw)zrztt7vy&>hl!zkLaEZ?%Ma{MA40OmXI z{9nL)SF;e^&`9&EZ70{f)LbXu7LbMmVDlr*b@JCTgk?s@%nW(xY}butC%?DbAK2X)rtu6J56 z5q&m^i6}IS3AGepHB8p>Ja8^nutW^m5E5dL}WF_ z@~-*L;e-qCe*-nWrpMmk)Th^YM#n5hK}2JlG1Ze!-8hxS(5t%Zjn0jd!OTZC!|!f% zPDsD5LazyIsL#}QO!Iyw^Oo7o$e zhtSsl%@CU3EZchfIX!0v1i9(S=4jwE+}#Vwe$p&QU;Kg|GSfR0A${gdFVh1dT>_Gc zkj^485FuSOI#x*MMV(7vJO6pzFw>ik9DeYn=F{<)dR;Eq@6$Epj6M{BOzczFxFX$` z$aYpqB;7lw%#zhKsq5TYC8?=#O{t!h{T<`ooIcE65qY+`GDk zlwrc}7Gn*zH=%!dIrb++;keuz?fx^Noh!UHJhr>S8&MV6u@O;2EaT8pP6@1AY!*&f zbjn#;?(o{aRx@QR){7ee=&i!2|kN zKl5%XFj73Vxlf8}F&RU%r+qUn+&5$OHIx{dfQEgkyTDMo}_t5j!t`gWhwEcS2O>ao2i1M!r#;>S_=xs5VZMGQh*fvQUmc zpa+blTB3;f!;@k&+)-4&erc*w4nlBz|F0!_>9yX77FQ-9Mtq5gqlh>LKp$U=8rddx z(A7W3s?)^Kyf~i`e(Uy3n-dgAN3KQGw zih17Qw!|*_@_F9T`P{bHEA@->nu_guop+h~JW&wVd)0rJr%qt=NMXKU)kW znPb2wz!xJ=alCdnkv?r)4oC4L;m_%fPV!M>>%y!g$}b`a*disx$5*B9|(Y zD4nq+3q|gBFrjXQoOi&0A#%N~MWw*2#D9?j3H{ZdQC=7sYPTX6Ii?=HkEZ1!RaURIQK7@C;!$eE1x z;w;6u9f}7Zt)jv{VquJG*B&w;YB@(UWouNde;5iq-JA?{qt=Q%IFnFi4y6Xgvx07+ z=|1+&d?w*fXlTGO<|3OUW=YHr>`@%Ji8bHa#9!VgJ90z5tBwDcP0W247+<&#Fn0Ps zYhunoxCxBQ4v6V9zZHxhN)y@7{|;0i5RAG1&rsd#Tidwee%jdgn@!B^OO6L@;R1_C$ed3SidJ3P@yBh0)QZr~w( zpw;yk>dkj}2X(q33S?F)M*9>ftO5Q|9&BjT={qT>zM<+)Z)}4CM=D7KE)ylvxNWU}qn^IZtKmpUm%A`7*tpEA^l#(Xh&g@3 z(gqQFiT>L%ueZ&yR1)pc92QH6drS0-cNuXjakp2GFn8m zb%@SPX~b%F2zh$mJskVFT6T}|iE9dxm2!jwAoGT;9m#Io~ucZ5G4JZ_yDIZembpjO`IU#@xxb|$iHs=#V}vOg^O=xa#$=9V z$W>CCg$Qf=;0z^ZPT0_-&0EX`_tAw(Q!+FZ8M8n{J zQP9{Vjos5yM0|5_w>Cj$v9=3r#e2f6_`nnB-`$B6g((woChEm?nCuvO0pw!6V|yXa=aL<3OUW)aK2o>o z^ZYQkoZgM{+GvDnvWmG+KI;b5BvHYlk}fDX*Ri&Y>|*?r&%V7NyX+v??8bF?Z3{As zC6HtW@Ly7QuQ-HRB@rxS-&-<6Exz@d$LiM&@QA|SS9xXHXANTQ_g9OHA!(ZNO=Jhd zzl)8QG{x*p*rNUsB39x(FuDr}Hu6@~G}fXwb1eR4sD-a=cMQe0%2r87mnP7Ta;0O& z>!0aTohL{zU9+8n$1wOrpb@x48Dp}}7o~zhVP(^Z%u{bp$alNr!XK()D|~SeY?@vv z1R$&;^7TaHE5*J}SVi;>TCFr=6OxEWZ|OnIjyV)PxGjh<)TcHp8xxdnyiyP!SgeO~ zf}m~U*eilSu#=~B{%IFa#wU`nD}m0QmNoIMNJ$X`LfPR2JB=lh@%ms{VZn%NAakA- z8poWL9U64xOoy8I;Xb_2@*RVSO;Xko5s+%lZuHYr8jrw_uRse)NE~hZW__ z&@XgC>I-W&H3=r|`ieItwFfnoFUEM`2*7!JzI;&%?aM(zHZNV6?^n{=s9@WLOy8W8 zd~j3A`wa1$G5E1jNZygK%@jT3$2se9pG{xtiF+@a@I3LqqaV1dayQAX3MO4f6IFq(M-|&3}alYzcQZ3bN zFfHp9Z*;O%G|L9c_|S~#b&hx}rLjHnUX5>l!!9^=W&py__ZORY^L>=3q}8KrPGv+R zQ8arBeK4aEXP;sXkLw`8CmunjqK1u&(ljS0_d{&@zcl?a6`*=Gq-y-~S@hMo8vu!P zV?9^HEm36oH!BO=_rt-v&M2os47p|@>G&4FYv?7=i$_3iKfN+%puCp287zVY%Eba@ zTrrSOTo}^W*Y10*_pwX}vcp!%`!VAF6wwu z&dMeX2JrLX*mtzr#@MOzbCoTExE+tz!cFiJACyh$<-}?nArE0N<-9^U%`3wJ66Ovv z229i;ctBvv7{#6dB3RP`u*;$yFyn8+^rnmk^EkWGXUDK4)h}R?_!2F6lvR{B^9pYrKKwjd&Sxy4Ne@bw29T%&*jmUY3b%I=kJQ6M3{v{+1zQ*Vz35nE5|2F7D%ph` zlV*njvjJ!ZPnaZR4TFvi$yvg|3$`H~#)lysW<-hg!nh5`tDR%>lSx%=)&}X7>AuWP zCCnt>LJ|of3E8KxBq8vbUD+aYsL(@;CVGGzX);ddOM*A(#8|v_qB=*B&%O(PCD9T> zLNBW$xH81FFY3|zhp10v@Q=+Fi5dHXdVro{!dL46_S&5nY_Jhe$JQz{cyoEGW_ zAu|1&8B@Ys3+pg=&b5F9gqwt1L=b(;0TJDV=hY#eR|=l-2vLCNrGjVn&cHD%6XC;k z;MnXqEEg6LZfJmG_R!vYN*$X+jE$2iL^5r)NCs;p$STqm7d`8xN=CpsxeBhOYUHeJ zXRd3ww&jY*cLK6=8pUC%PBT@=f5}A%vzQ}(3Bq}HyD3p5BsmYV z4ki=Nbe4Fv(3POs&2*OSfw+(_VO#wy`-#2c%m#hrTIXl`@>xbIv*FY-mDLhXjlz1x zdzwkd#EY>3-P$u|5F<&HlfT04SK{;+!V0TmQkg?yCc(kLI=P_`hP8+&!hX3fm20(` z?-|~6ktNCF?roXvpQ&EK99onu5!O~XjYzL$5)s4xIY@K~+)NjA&p7%w<(o;)bgwRF z`bRhgKbw)pF88xpY0OGLE2pePUTB?Z8+!YZ*{TVQRMSho11>$?CBF2MU|ha!D6?sQ zc@Nw~r$rPw2{>S)$Vu~d8Spct&Hjt+|DF~#@tySAOaO^Y>#)-OK}X|ul2hOl>@34) zQm7y0v5rTRJr8FxFD3IFp}oemr1D8raHea|81=`V5~3#d(tAt@8`7B(*`b+iwtXg( zZEf9BU?`F&u{q8+xNF81^2j=l{)HLT}=pBh=H7qpxLNpac!%T&$H_74;|8JO)OP63rOZUma<)V&v+&FL;*FoC>X2|D@2u(o?2=wfJQ%9p-jF| zYJN>igWiK12zAPiJv^+LPbHE`2Pu0npJxsW<0{R~uXZUYTI*IK3D8zd68@dGHd7zr z{lcaeERmaPkP3vs9vB$8kabyTG*g3^4_II@pGDxV57*F5VudW~4qQv~m9P@EwA&Bn z6x9;3C?NICS~~m(wKP>ih(Ftvl(6VvSI&%nT3tB$*9cQ7%k;?}mT8&oPzvEkosQHW zLILfB7$UgA(kQPP_(}Y~Oj9AR6p&GlIna#VehjeUQv9GqRTbtUIzX670_x!t}Li=!{q>DQY7p{;6NsPgB1-;8DSAf4TBlX-&#aEEW$7uDI(|iH+=&<%o5)Ll`^^_ z{*Xd3;hRM;cYH??Ehs|V$QVnpJhwY3oraU<55-uXClkTRd0uL~EZOdFfS4l{8#EH^ zf|}bFGQ75i;X%t}TkU5Vp}l0b9xPX8-$OGw64v`!gTxKKgrVr=Zt}B3xQc?UdIbxr zlB3dXGSy*}Sn|U#Wk89T)2|Sr+d{fdLy7rZvNY}|&rl|enNCOOLzHs-T>c|DNhjRH zr~RhYB3=0r!0H*k&+@lY+#rN zp-4C14niCBug>aq1b(^UW=qNd(8y!}5Xs*F&;WEj02S6G?-n8x^vQHgWmTq4HjSs7 z(##M8|8N)-s*+1-3-aWF$Wf$KLyiEG`4@WZBc(%+rUixM#e2xmN`;Uqg`&0bshj+e zX$=IGvTn!hfcR5Hf)P>lhX_)c%0kf33vp;vEjbou$e?+0HAEfN%9LcvS+Wo6_W7RA zjTCmEMiWmq`g|XJq^ybS^?%te+`X5}*>lBgoR6uI*a1wvsv8Cb|h zaoOdJm|s2@fGEgCEGo0H8PY~w2zAr|Wp-Gzd@7Trlo|1M`URcw!u_oLM}&he_-Nm6 zh)r3z$Fo0ru!xpcp`11qnq@VLhH$OqNk(5$wi{yH@J!$AFuN?{JV_q-Gt+T57f!VT`b*2i z16F~SJx|%5fuaFbAzm&^85RqP78iv_QB{A>0@l?+Hbk4K7SGenzMZmx+^BrB8kfItM^ z>TLgT^cJjR>o}wrhGj;ZZ287K6Y9HQxUE{7638IlepW>f*kKKBrv=XU8Qa1quNfe@kR`W6N!^o#iNs%H>`WfHEmaK&(vS_L;ik8xoa=)HtItoLRAIoldmA?(75GeGQrl)){Js#LNuU@9_7JoKS^`6efNV%gWoB!~te zq>N^dJ(D;}aNN$FS4kAm*;xXVfGaCmPFLPRiy0OD*3KOwFhel$#;$rc_3y<6?Ti zb_$wt6wavdsfrUDpjYr2Z8eCNmz9?*THl3SWhmCk)hGkmT#$P@T92yLbSp^V0gBYd z90*356Zy*rz?kbL=>XR)67!Dh<|NW4SNKKTe~-;43zk;&HFp`bFx)v06qlZe5=Gvj zA#XJ`gHOUvV%`btWRIN@$Y+^W@MjJmM*8I%Uu|#V&7j`JT;Rx zrSoXJES&>Y(&2R2VSLbJl=)WL;MoE=YM{iQl(JMYHDS|pGQQXmH?#XvA7(=69^c!9*1Fl zQ^{>U;V31xZbbg}Uza%`Z&G2lW%FDhU@f|uX*J;*#)aCPRC4FH>9#iVcZ69CoDgP8|`8m$bf-Q%%gD)F-ga5AOGS4+nLDi;7>)2GZH~Ypk9KN0VpPNEx54dV!Fhv#q5|Xgy~k^wTG7!z;p|* zh$LSKQ)OT(Q%Y6r*%G0ds@R7jTwt*P7W`PF7TJB4H1uU11W-UJp6M_p z;{lgUF@L6nU=g_@lrm(l;OlsR84qi3Yss<{(HxS76htHw4fS#pZj1VI&3|=xX+T~k;tr?_l*s|Or0K&1E7$}k4Vq4QP=;|*|vOIuu zRmHwa9L?<^7Fh-f9A^BK5U=NZJjylh^cy?+VmML z_(CVqsZ(ZAkV{;4VsNwj2{>vqdghy6x12P?EFT22s}XkV%p|cifeb_UmnZBbywXTc zV)tlF_EsjQ1apjm98?_&>*7O9j0j7a2f{QsH7{iLsaJUsT_9ek*hdz&l&dE$F(*Hp zzNq5`MZozZgp-rS7nBst!2v-U+Q=8bM|fSPv@r9WcujnllL1NMm&j~nzNa}7r@$g+f`QMe&q+tY?85R!6;yC@jUQ6`mU)UK!CMjOSXDPOIet0lcX!$;@7WUpOvxU!_ zQCOL41IMLRsj#GYrhRzVI@2*KU7Ftrg~6&@W#j|H@nT}R0tk@M9GUA`3viG+G$nur zL~Hp?kzb^B$1Y(I!P@Ex?|=lE|G}xzcc77zC6Hix4^|lo;3Cr;qo$P|*cdLD$#oAA zT_V=IGC2i{Ir_xfjz}D$73sk^7%j?9GqZ!{E)Qe|<~3n`q)(Az2qHi;lwhJL%Jx7j zG+MTIJ2HL-XZz%J+J{oqkW{vYN<@Qgwp9wrcW0TgxF~)AT|=mHbtthf+bN~YENP_o z8g@|nu+5^f9fe!k>*BMF|6eQZh4usj4Qb39 zO3{<3EL$$tFe70D2yaFF=w&M^Y|$Lxv_)!w{o8tioDFLwf-x2pLt+a^7L0F5Lq*)P z8I{KRjqBPpqjFyxzqM)1Q>eh&__Y%oKbewdS9Z#@<9{dYxwx8B*PX;VDchT;&oEdr z_FV0l*os>kgRWvp-N&G-U8XlY>wva=i43b_G3aW>mJcyO&TL4_vtj{@EuX}DrUn_O zC&-AZx*#(b_l**(o)Vigt#>Ms`GnAORTsUqy zf(wT%htkMFQ!>3mC}T~{qQ$^BG*3b792T1s&iD0YLtFkY&_zxnexHl&ev`psfL7`xp=<@#8yfl zMLy-PbW3Ort(CPR4N9=I1KI8AGK`XUy*Mq@?)P(oi=ibMoFfZVJ=7sS-NpLO+>LEw@ zsC}D>5MtjM@hLFzZJYq_w`5{DOQwIx!NhWVCx<2p-Y1C?GF5~R=0Gy7O30h`yfMKz z%^NZgV16gJd}fNPq+(k(sES(gk)(qbDQBg=bq8LBp4Ct6@OqTr_=;%MVtfjDO9_k? z{f`}9=Wbv8K~ey%1Vil3#k!TrFuo9a`tqWy9`*qTgdgKGj)wwFE}ih?GVKHWMAg5n zfBAuTx^sKO#~*n2JI;d*`a`d!({5=p>k9J-HpE1_0f*fAcQrh*(|g4!{}qG6Uz^Ax zt^_@WgW&VA;ogtDdmUnxAODHhzQbztDI~EPbL1iej7yT}lt25#>zFgiB55*i8J!K1 zTLPP*eg`j)2sBrq9}H_nHDxDqL0mqzHh&r8^{Sq$@ucVDA>uP zLoSPS{(1ob2 zTgXQ=Kk71VUCCo9SmZO#TtuHi$?}WB9m-kk@d^ZyqLkw0y z`voM9|2N&obF;ejZf_MHsvdkKUZp?U?X`BE(*N3xx0t(ho6o)O?$ZhVc#&JF&;16|Z!iTRxV`U`J{^LssG zkGI%)QI~w_4fCEs%a0GWF<*LFzMAr-_g`f%nTV83*zgv@|9$$UH_3TgkNXFe-K}r_ z2j^{{(rv%OujM9v=vRb+d{j^S%A1{e)*MiU%!R@^%3bg%4CUEHdx^3BB+g@mt;p~c zy{o>z*zKZ6@AW#TA4!BhUy4S>QhLgBdDRE zLWl{jFRgn?2mkUK-E|4Q{;v4JdcfD-)j;pzuf5Oxr^HO0Z3p3a)Vk@Hzf*&|(m9kV z{+5K08jXY!VHGr!!te#4%N%uO=`(CJp??tAZqm0os$Y5&gGjWPP^ABie%(Co7KRzL@C9$4qT@0 z>E?3*kwf30BJ%!8zphkY=TACb0%dH|y-QR@hd(4l7KmyB;s&bJr_U=M{Mdgc{XqyBUJe z`P&JRUE==xdUR6tEZZTFqP4V?H2rcZ+aEH5piqLQ=zk_urEW~B(&PvE0`$8{rjGaZ z*GV-L&w4{sYUr_+%)MYaC7JSs(CwWOsZ9!X^NKl=ztUSlwL!hbl_5I+HT`HxZKT;h zEmiHo)}m51Kq_Zurq!l}V=MBl7JfmGSj9pR(qu>9QmQig^t5VQ*ym`_{WQ&xc}A~J ztFueqf@zqMSXQPcA7&vp$DnyGVbu&E(E|a_-A)8o^SCy8TxuSd^QhOBsS}Y-idv|i zV5F*r8kl-o_zMChtVK_4p~h0}!!1;G@w>vC7qwJv7x}7R^3QNHz%4x+>Kk;8_fgRpc$9*@U-)l>xv1CiOh#u2NwN#b8 zn~G1!W>K;Ke!^&35zv9C<2RL-ucxeU(@RqYlY##(ovDhIB9qZ9F?&Q@lo|hh{ZUIb zxZ^g__Q_;pge0QXN%IB0Z|g%_sbh)-F)95oAAY^Gm8wndw7nLJ`=piXMr-}nYEbeo z_A}S^`d}b-Zd+B=&b&tY5=_HnS+m6qpM9jYn(w@0$S!KD`tH|-?s1t9n(c&41IyZ~DIFs4y8D3F1-#wcsiOhUw07#y4vb-; z6$6I?WCkU=WqZ{|*SAv%Ul1Gj882Z@i`Ha&)vFzYCjWT^Mrpw@?bQhHcOrB@ujjW{ zk9U94fEm@73}SbbsQ3}lWv({rW0!TyGkSUl)wRuw_82NNrkxY19Vp(_0S5bM!v#NGJ)~&m!quYE8u~1A@2*pVuGrFkClKOWx z%5v#FeM=W5($}M1)aagwI}oTLsqMBsGBWIoFirT@cHOh9>XzgUGBVFQ^yyvIjD-CJ z)?u-_y{l@K7Va(477;`NJ-Yu+IH2v4QQhGC#x*6CS$Rx^l+!Zm?A#F&VMf-0S%~{T z3^X%B263#`y|$7+bEO7o>wglmj^~JX1~-YhYksoNYcr4})4_?<1naS@+NdpnUIw9> z_TR}&W0cHtulrT1{?0OeUZt8Rp0)l`sd~w^yc-Jxu0z7>xNho9d48yys*vl8-PBOI zmUM^laUI-Ujg#w5-PIv-eX6^<$^FvNryZnbxO4uXpFT+4@6M^yvkq3>+`2lg4_4is z|JLgdRuj`p!J%1S7Y8ff(fxX;gGf28hdS(#PZJOPG@FKT25gfe#(bJcyB*C6J|nBk zVB4A}WBp^X;W3zD;7$p)uhCESfDAuv_;U~H>--snGX>98d|E3~nXI^5a5sIO{Y zW_{&?CdQ}#p+2~;8s6doCv_0O4@!c?asTs#p4nF&p1xNYqp-)g|F498v@d|ONgcIY zrG9SHt;bZ-7gec4ttVFT;WeS3`mMWXjqcE2)l~d7VWJ)z-9$~iT9yGefZ4)_^wcne zSoIz-!((9v(M3p`*4TfaxJPrDHlSDy#xU{mV-fEqdw^nHK5b8FeqV@ zQY+)?OB1EX6ur1n-+l;#Vs}End?hn{no8p&nDVd@^| zm-?c^)fS#=Myfiuey!d(QW6`SQR*J|pr!a8uC~rW*NFW9`g_@ft91QalT_ex4xUB# z3$^O3gG5^aOO}0ZBw(gU;jl$Ui6JX!#B5^m=&?tr!ENt=#N)v#=R}sfG7|}hp_qk7 zs4>p1`kf=d;$6Dsk*agM24OeiAeQs?rmm!|arBYu2Tnu7qerUk#iezRx+liixZMy- z{j*`!F|^w)THbKd@oHzWesiHZs<^Jij~%D`pQ?^<-qx3$s-ndt z@4HJE>0PI(XWjb6`jOM2JikN zo^b}Cdb{DlGt`|9E2$I5sbTJ&cQjl-PW?3Du75};zprj~Ue`;$ua+b>ckpAaDB)cl zT%an5kZ|q=s%xTsM?cmk7CgCDUvq(~>^z3YG?`B48s|EWYcW>@jBO9=y%(T$AaIDe zZg@lwx=>x@Zuy;kUh}Aa^+I)mw{<-|8?QG^q-WzMGS(J9uIEow7kOKsFbV5VBcbO- z>JVqA{@z8Z#%tWj`)c{%5O?L1dhFVKF4SEj(+cA>R3G)u&#VT`%_HDl~Yus_{E;eWse>~#+Cr9eP zUI|>gkJP=dQWp`i;pVFt-f!y1u2Ofpi?7t@U9H-V_~VruV=1J@;GgEki3N+Vk~iG! zk(+;?Y&E}K04;><;pCr-OCGd^~C;Ky1}e$jHhh7yvC!{Jmaq6TAvuN4uiL=IDs87B@bspXRSPS?4uRqRYGC!4ukS_Q* zJj^#Td_EVenZuUbZ_m-cxj`L$=;>rTb;bo#C(aHoyl~c-8JEqT#4i>*gZI*Z>HC@d zX3m&3yRfO_^uQa{k8=W&Q)kbZHSxH~mrS00*##43PPhQX@6)TZ$aBer%g>)UefF#? z&Yw1M`Y1?S_#3-@Wrxx62J$;{bToHSwfq|wrv z(`L-5oqTa&zs@7i@e?jTBJ9ZVmeNk0c=07Pf9&)hN(=UVI_9z&(}T+{1Ay}9$cY!} zih6ZWy0Fmm^@;VWqStuJK562_S*OjKeDTE-XPq)*+Qd9+yO9g;PiMB z5xHX*kk~=zZYNNHw^)_wk+-XX$&DmdllYS}^iOVAQ<7EZ__3ipFQVq#)hWqadG5{g zx-;}~cc>GRyLj%y^X4=3a(Nzeu6=&*488XbH6`&Yo)0Dc?|R0a>eOW0^ZZy9j~&j` z)vHi4rI#wD|8^%8|9}*!xSEuuYI^!(o)6*qm@}Ef^!ZCvNB!|q)k1%KmrCk)m#V($ zGtT#8gUN9bIl4+GyXf;se8@7tVh#_6~3R!1d&%(G0Qg!8Og;w3s$cjr%$>^otONI zXMx|Xfqw9DX+DD^d8ks|L9)jyRFXB^Y10!z_awGds&vB zepcAjWw)tLo{X@GXK8;0B=GP=+xIKZ($iK@bGwTy%5OSLZ>4$tZA#rF9{YG`FK+hP@DuxBO8ydRM$1hVZcA&|2ZZ}2RD{6a5SrF!&k z3E_)8BCksM2~!=w@0a`r@{0z3?Q*?imFnGU_9QC-9R;rzKF++Bxli>@uH?OZzjji? zY4;&Ny9-~}vsbGg?U(qD zc{xeAYhgz+y(J07qdr(tO|Fm4Lw%P`>)XbHugt@sgFm0zDHdFJ9of7&+iI?Oil8L7 znx~~(uQ_uN8SgVsjVD}u$;OIU@H_K#*v;d2j3eiB@}x%1fB5pw4{%kG#5OE$ShSTR0Pdo-4R5@mUP<(O?ruL;M$SKSl`k%0wwf(@XU?Fq%=q!V+fAB` zJJA2;`|5kC<do4I+gSiiSJRX42Kp+-2PyQiUdek1rD z&+i<5m+-rS-&Op6!EXt_Rs4R#?+Jd-^ZNt8xA^^;-^aT5pVgV2MYa+7NPa?N=Zy4Y z7x0_L?}z%vKdX@^3RQ|&6z39#9r*R&*O%WR{6_IRj^8+b S=kxnMzhLjny8J`+)Bgvi6=YNZ diff --git a/configs/swarm/genesis.json b/configs/swarm/genesis.json index 432cb2ce7b2..c952e7dd08a 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 7efaba05a69..179b5d6024c 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/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 46870b8254a..4278dd67229 100644 --- a/core/src/smartcontracts/isi/account.rs +++ b/core/src/smartcontracts/isi/account.rs @@ -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 diff --git a/core/src/smartcontracts/isi/asset.rs b/core/src/smartcontracts/isi/asset.rs index 18cdedfae47..d3221810474 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 b1752581779..81d80232226 100644 --- a/core/src/smartcontracts/isi/domain.rs +++ b/core/src/smartcontracts/isi/domain.rs @@ -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 @@ -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 57e82ed0994..03e15e11d27 100644 --- a/core/src/smartcontracts/isi/triggers/mod.rs +++ b/core/src/smartcontracts/isi/triggers/mod.rs @@ -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 5e26fb4e2bf..ff65fdee954 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::*; @@ -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(), @@ -105,7 +101,6 @@ pub mod isi { } world.domains.insert(domain_id, domain.clone()); - world.emit_events(Some(DomainEvent::Created(domain))); Ok(()) @@ -306,43 +301,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 { + old_value: Parameter::$container(iroha_data_model::parameter::$single::$variant( + prev, + )), + new_value: 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 { + old_value: Parameter::Custom(prev), + new_value: 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: 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(()) } @@ -406,7 +428,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}, @@ -484,11 +506,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 64f385c038f..733deb3ccd2 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -2,15 +2,15 @@ //! `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::{cursor::QueryId, QueryBox, QueryOutputBox, QueryRequest, SmartContractQuery}, smart_contract::payloads::{self, Validate}, @@ -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 42c43d98174..2c711280929 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 6d75fc54358..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? diff --git a/data_model/src/asset.rs b/data_model/src/asset.rs index 005911bf1f5..7e4e1c43f9b 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 7a3f6781453..f355b462a4b 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 creation_time_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, creation_time_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 98c703f3fa9..92a841a0e61 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), #[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), } } @@ -243,14 +243,12 @@ mod role { #[has_origin(role => role.id())] Created(Role), Deleted(RoleId), - /// [`Permission`]s with particular [`Permission`] - /// were removed from the role. - #[has_origin(permission_removed => &permission_removed.role)] - PermissionRemoved(RolePermissionChanged), - /// [`Permission`]s with particular [`Permission`] - /// were removed added to the role. + /// [`Permission`] were added to the role. #[has_origin(permission_added => &permission_added.role)] PermissionAdded(RolePermissionChanged), + /// [`Permission`] were removed from the role. + #[has_origin(permission_removed => &permission_removed.role)] + PermissionRemoved(RolePermissionChanged), } } @@ -297,21 +295,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 +385,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 +484,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 old_value: Parameter, + /// Next value for the parameter + pub new_value: 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 +666,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 6d74eec41e1..bbecb5ac47f 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 964d9f20d8d..5309c6ed477 100644 --- a/data_model/src/executor.rs +++ b/data_model/src/executor.rs @@ -5,20 +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::{Ident, IntoSchema}; -use parity_scale_codec::{Decode, Encode}; -use serde::{Deserialize, Serialize}; pub use self::model::*; use crate::transaction::WasmSmartContract; #[model] mod model { + use derive_more::{Constructor, Display}; + use getset::Getters; + 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. /// @@ -78,19 +80,15 @@ mod model { #[ffi_type] #[display(fmt = "{self:?}")] pub struct ExecutorDataModel { - /// Permission tokens supported by the executor. - /// - /// These IDs refer to the types in the schema. + /// Corresponds to the [`Parameter::Custom`]. + /// Holds the initial value of the parameter + pub parameters: CustomParameters, + /// Corresponds to the [`InstructionBox::Custom`]. + /// Any type that implements [`Instruction`] should be listed here. + pub instructions: BTreeSet, + /// Ids of permission tokens supported by the executor. 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`]. + /// Schema of executor defined data types (instructions, parameters, permissions) 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 0ceeb0589ba..67ac4876d24 100644 --- a/data_model/src/isi.rs +++ b/data_model/src/isi.rs @@ -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::instructions`] + /// + /// # Examples + /// + /// Check `executor_custom_instructions_simple` and `executor_custom_instructions_complex` + /// integration tests #[derive(Display)] #[display(fmt = "CUSTOM({payload})")] pub struct CustomInstruction { @@ -1236,7 +1220,6 @@ pub mod error { use super::InstructionType; use crate::{ asset::AssetType, - 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} @@ -1513,7 +1494,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 55cf4c4828e..5b7417b53fa 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. Permission(permission::Permission), - /// [`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::Permission, 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 6a7cfef7677..04bab19588e 100644 --- a/data_model/src/permission.rs +++ b/data_model/src/permission.rs @@ -10,13 +10,12 @@ use iroha_schema::{Ident, IntoSchema}; pub use self::model::*; -/// Collection of [`Token`]s +/// Collection of [`Permission`]s pub type Permissions = BTreeSet; -use super::*; - #[model] mod model { + use derive_more::Display; use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; @@ -38,7 +37,7 @@ mod model { Display, )] #[ffi_type] - #[display(fmt = "PERMISSION `{name}` = `{payload}`")] + #[display(fmt = "{name}({payload})")] pub struct Permission { /// Refers to a type defined in [`crate::executor::ExecutorDataModel`]. pub name: Ident, diff --git a/data_model/src/query/mod.rs b/data_model/src/query/mod.rs index 639a1d8d160..4520069e0b3 100644 --- a/data_model/src/query/mod.rs +++ b/data_model/src/query/mod.rs @@ -212,7 +212,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), @@ -221,7 +222,7 @@ mod model { Vec( #[skip_from] #[skip_try_from] - Vec, + Vec, ), } @@ -402,7 +403,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, @@ -429,17 +430,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% @@ -468,7 +470,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)) } })+ }; @@ -490,7 +492,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)) } } )+ }; @@ -504,7 +506,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),); ); @@ -521,12 +522,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()) } } @@ -854,7 +854,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)] @@ -1536,10 +1536,8 @@ pub mod error { Trigger(TriggerId), /// Role with id `{0}` not found Role(RoleId), - /// Failed to find [`Permission`] + /// Failed to find [`Permission`] by id. Permission(Permission), - /// Parameter with id `{0}` not found - Parameter(ParameterId), /// Failed to find public key: `{0}` PublicKey(PublicKey), } diff --git a/data_model/src/query/predicate.rs b/data_model/src/query/predicate.rs index 47e16e6bd5e..3846840af0b 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::Permission(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 ae48c96ff30..20920ee1ab4 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": "Deleted", "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": "Deleted", "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 }, { @@ -1204,12 +1194,16 @@ "ExecutorDataModel": { "Struct": [ { - "name": "permissions", + "name": "parameters", + "type": "SortedMap" + }, + { + "name": "instructions", "type": "SortedVec" }, { - "name": "custom_instruction", - "type": "Option" + "name": "permissions", + "type": "SortedVec" }, { "name": "schema", @@ -1486,14 +1480,9 @@ "discriminant": 10, "type": "Permission" }, - { - "tag": "Parameter", - "discriminant": 11, - "type": "ParameterId" - }, { "tag": "PublicKey", - "discriminant": 12, + "discriminant": 11, "type": "PublicKey" } ] @@ -1721,9 +1710,9 @@ "type": "Permission" }, { - "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" } ] @@ -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" } ] @@ -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": "old_value", + "type": "Parameter" + }, + { + "name": "new_value", + "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" } ] }, @@ -2914,33 +2848,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" } ] @@ -3288,12 +3227,12 @@ "type": "RoleId" }, { - "tag": "PermissionRemoved", + "tag": "PermissionAdded", "discriminant": 2, "type": "RolePermissionChanged" }, { - "tag": "PermissionAdded", + "tag": "PermissionRemoved", "discriminant": 3, "type": "RolePermissionChanged" } @@ -3324,11 +3263,11 @@ "mask": 2 }, { - "name": "PermissionRemoved", + "name": "PermissionAdded", "mask": 4 }, { - "name": "PermissionAdded", + "name": "PermissionRemoved", "mask": 8 } ] @@ -3508,14 +3447,7 @@ } ] }, - "SetParameter": { - "Struct": [ - { - "name": "parameter", - "type": "Parameter" - } - ] - }, + "SetParameter": "Parameter", "Signature": { "Struct": [ { @@ -3590,15 +3522,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" } ] }, @@ -3675,6 +3621,12 @@ "value": "Numeric" } }, + "SortedMap": { + "Map": { + "key": "CustomParameterId", + "value": "CustomParameter" + } + }, "SortedMap": { "Map": { "key": "Name", @@ -3720,6 +3672,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": [ { @@ -3753,7 +3731,7 @@ }, { "name": "block_height", - "type": "Option" + "type": "Option>" }, { "name": "status", @@ -3769,7 +3747,7 @@ }, { "name": "block_height", - "type": "Option>" + "type": "Option>>" }, { "name": "status", @@ -3785,15 +3763,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" } ] }, @@ -3825,7 +3817,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 367459d8760..4e846d0aa72 100644 --- a/schema/gen/src/lib.rs +++ b/schema/gen/src/lib.rs @@ -209,7 +209,6 @@ types!( 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, @@ -329,7 +322,6 @@ types!( SignedQueryV1, SignedTransaction, SignedTransactionV1, - SizeError, SocketAddr, SocketAddrHost, SocketAddrV4, @@ -345,7 +337,6 @@ types!( TransactionEvent, TransactionEventFilter, TransactionLimitError, - TransactionLimits, TransactionPayload, TransactionQueryOutput, TransactionRejectionReason, @@ -430,8 +421,6 @@ pub mod complete_data_model { }, InstructionType, }, - metadata::{MetadataError, SizeError}, - parameter::ParameterValueBox, prelude::*, query::{ error::{FindError, QueryExecutionFail}, @@ -444,8 +433,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..14ee485351d --- /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::UnknownIdent(alloc::string::ToString::to_string(value.id().name().as_ref()))); + } + + 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 bd01de26da0..0208209c2dc 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 { + parameters: BTreeSet, + instructions: BTreeSet, permissions: BTreeSet, - custom_instruction: Option, schema: MetaMap, } @@ -199,8 +201,9 @@ impl DataModelBuilder { #[allow(clippy::new_without_default)] pub fn new() -> Self { Self { + parameters: <_>::default(), + instructions: <_>::default(), permissions: <_>::default(), - custom_instruction: None, schema: <_>::default(), } } @@ -223,26 +226,37 @@ impl DataModelBuilder { /// Define a permission in the data model #[must_use] - pub fn add_permission(mut self) -> Self { - ::update_schema_map(&mut self.schema); - self.permissions - .insert(::name()); + pub fn add_parameter>( + mut self, + param: T, + ) -> Self { + T::update_schema_map(&mut self.schema); + self.parameters.insert(param.into()); self } /// 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 { + pub fn add_instruction(mut self) -> Self { T::update_schema_map(&mut self.schema); - self.custom_instruction = Some(T::type_name()); + self.instructions.insert(T::type_name()); + self + } + + /// Define a permission in the data model + #[must_use] + pub fn add_permission(mut self) -> Self { + T::update_schema_map(&mut self.schema); + self.permissions + .insert(::name()); self } /// Remove a permission from the data model #[must_use] pub fn remove_permission(mut self) -> Self { - ::remove_from_schema(&mut self.schema); + T::remove_from_schema(&mut self.schema); self.permissions .remove(&::name()); self @@ -282,8 +296,12 @@ impl DataModelBuilder { } set_data_model(&ExecutorDataModel::new( + self.parameters + .into_iter() + .map(|param| (param.id().clone(), param)) + .collect(), + self.instructions, self.permissions, - self.custom_instruction, serde_json::to_value(&self.schema) .expect("INTERNAL BUG: Failed to serialize Executor data model entity") .into(), @@ -309,8 +327,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::*; @@ -321,6 +339,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 dd6f729e900..a402e861c32 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 { @@ -113,7 +113,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 a5fb95f0c94..1004a9f57d1 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()) @@ -135,7 +126,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()?, @@ -148,62 +139,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(), @@ -212,7 +147,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