From 53e86a1ec9c6d2549c3b83f6d180330b51678e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Ver=C5=A1i=C4=87?= Date: Thu, 15 Jun 2023 10:26:22 +0200 Subject: [PATCH] [feature] #3468: return iterator as query execution result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marin Veršić --- cli/src/torii/routing.rs | 47 ++-- client/src/client.rs | 24 +- .../integration/triggers/time_trigger.rs | 10 +- configs/peer/validator.wasm | Bin 590794 -> 590252 bytes core/quang | 10 + core/src/kura.rs | 12 +- core/src/queue.rs | 6 +- core/src/smartcontracts/isi/account.rs | 114 +++++---- core/src/smartcontracts/isi/asset.rs | 208 ++++++++++------ core/src/smartcontracts/isi/block.rs | 37 +-- core/src/smartcontracts/isi/domain.rs | 35 ++- core/src/smartcontracts/isi/query.rs | 130 +++++++--- core/src/smartcontracts/isi/triggers/mod.rs | 39 ++- core/src/smartcontracts/isi/triggers/set.rs | 13 +- core/src/smartcontracts/isi/tx.rs | 143 +++++++++-- core/src/smartcontracts/isi/world.rs | 90 ++++--- core/src/smartcontracts/mod.rs | 21 +- core/src/smartcontracts/wasm.rs | 19 +- core/src/sumeragi/main_loop.rs | 43 ++-- core/src/sumeragi/network_topology.rs | 17 +- core/src/wsv.rs | 234 +++++------------- core/test_network/src/lib.rs | 18 +- data_model/src/metadata.rs | 12 +- data_model/src/predicate.rs | 19 -- data_model/src/query.rs | 68 +++-- docs/source/references/schema.json | 48 ++-- schema/gen/src/lib.rs | 7 +- 27 files changed, 815 insertions(+), 609 deletions(-) create mode 100644 core/quang diff --git a/cli/src/torii/routing.rs b/cli/src/torii/routing.rs index dfc6f504355..bed382b7bc7 100644 --- a/cli/src/torii/routing.rs +++ b/cli/src/torii/routing.rs @@ -5,7 +5,7 @@ // FIXME: This can't be fixed, because one trait in `warp` is private. #![allow(opaque_hidden_inferred_bound)] -use std::{cmp::Ordering, num::TryFromIntError}; +use std::cmp::Ordering; use eyre::WrapErr; use futures::TryStreamExt; @@ -15,7 +15,10 @@ use iroha_config::{ torii::uri, GetConfiguration, PostConfiguration, }; -use iroha_core::{smartcontracts::isi::query::ValidQueryRequest, sumeragi::SumeragiHandle}; +use iroha_core::{ + smartcontracts::{isi::query::ValidQueryRequest, query::LazyValue}, + sumeragi::SumeragiHandle, +}; use iroha_data_model::{ block::{ stream::{ @@ -25,7 +28,6 @@ use iroha_data_model::{ VersionedCommittedBlock, }, prelude::*, - query::error::QueryExecutionFail, }; use iroha_logger::prelude::*; #[cfg(feature = "telemetry")] @@ -71,43 +73,34 @@ pub(crate) async fn handle_queries( pagination: Pagination, sorting: Sorting, request: VersionedSignedQuery, -) -> Result> { - let result = { - let mut wsv = sumeragi.wsv_clone(); - let valid_request = ValidQueryRequest::validate(request, &mut wsv)?; - valid_request.execute(&wsv).map_err(ValidationFail::from)? - }; +) -> Result> { + let mut wsv = sumeragi.wsv_clone(); - let (total, result) = if let Value::Vec(vec_of_val) = result { - let len = vec_of_val.len(); - let vec_of_val = apply_sorting_and_pagination(vec_of_val.into_iter(), &sorting, pagination); + let valid_request = ValidQueryRequest::validate(request, &mut wsv)?; + let result = valid_request.execute(&wsv).map_err(ValidationFail::from)?; - (len, Value::Vec(vec_of_val)) - } else { - (1, result) + let result = match result { + LazyValue::Value(value) => value, + LazyValue::Iter(iter) => { + Value::Vec(apply_sorting_and_pagination(iter, &sorting, pagination)) + } }; - let total = total - .try_into() - .map_err(|e: TryFromIntError| QueryExecutionFail::Conversion(e.to_string())) - .map_err(ValidationFail::from)?; - let result = QueryResult(result); - let paginated_result = PaginatedQueryResult { + let paginated_result = QueryResult { result, pagination, sorting, - total, }; Ok(Scale(paginated_result.into())) } fn apply_sorting_and_pagination( - vec_of_val: impl Iterator, + iter: impl Iterator, sorting: &Sorting, pagination: Pagination, ) -> Vec { if let Some(key) = &sorting.sort_by_metadata_key { - let mut pairs: Vec<(Option, Value)> = vec_of_val + let mut pairs: Vec<(Option, Value)> = iter .map(|value| { let key = match &value { Value::Identifiable(IdentifiableBox::Asset(asset)) => match asset.value() { @@ -137,7 +130,7 @@ fn apply_sorting_and_pagination( .paginate(pagination) .collect() } else { - vec_of_val.paginate(pagination).collect() + iter.paginate(pagination).collect() } } @@ -167,7 +160,6 @@ async fn handle_pending_transactions( Ok(Scale( queue .all_transactions(&wsv) - .into_iter() .map(Into::into) .paginate(pagination) .collect::>(), @@ -348,7 +340,6 @@ mod subscription { async fn handle_version(sumeragi: SumeragiHandle) -> Json { use iroha_version::Version; - #[allow(clippy::expect_used)] let string = sumeragi .apply_wsv(WorldStateView::latest_block_ref) .expect("Genesis not applied. Nothing we can do. Solve the issue and rerun.") @@ -422,7 +413,6 @@ impl Torii { } } - #[allow(opaque_hidden_inferred_bound)] #[cfg(feature = "telemetry")] /// Helper function to create router. This router can tested without starting up an HTTP server fn create_telemetry_router( @@ -458,7 +448,6 @@ impl Torii { } /// Helper function to create router. This router can tested without starting up an HTTP server - #[allow(opaque_hidden_inferred_bound)] pub(crate) fn create_api_router( &self, ) -> impl warp::Filter + Clone + Send { diff --git a/client/src/client.rs b/client/src/client.rs index 08c4c60b525..f1b48c7d2de 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -105,10 +105,10 @@ where // Separate-compilation friendly response handling fn _handle_query_response_base( resp: &Response>, - ) -> QueryHandlerResult { + ) -> QueryHandlerResult { match resp.status() { StatusCode::OK => { - let res = VersionedPaginatedQueryResult::decode_all_versioned(resp.body()); + let res = VersionedQueryResult::decode_all_versioned(resp.body()); res.wrap_err( "Failed to decode response from Iroha. \ You are likely using a version of the client library \ @@ -143,7 +143,7 @@ where } } - _handle_query_response_base(&resp).and_then(|VersionedPaginatedQueryResult::V1(result)| { + _handle_query_response_base(&resp).and_then(|VersionedQueryResult::V1(result)| { ClientQueryRequest::try_from(result).map_err(Into::into) }) } @@ -238,7 +238,7 @@ impl From for eyre::Report { } } -/// More convenient version of [`iroha_data_model::prelude::PaginatedQueryResult`]. +/// More convenient version of [`iroha_data_model::prelude::QueryResult`]. /// The only difference is that this struct has `output` field extracted from the result /// accordingly to the source query. #[derive(Clone, Debug)] @@ -249,12 +249,10 @@ where { /// Query output pub output: R::Output, - /// See [`iroha_data_model::prelude::PaginatedQueryResult`] + /// See [`iroha_data_model::prelude::QueryResult`] pub pagination: Pagination, - /// See [`iroha_data_model::prelude::PaginatedQueryResult`] + /// See [`iroha_data_model::prelude::QueryResult`] pub sorting: Sorting, - /// See [`iroha_data_model::prelude::PaginatedQueryResult`] - pub total: u64, } impl ClientQueryRequest @@ -268,7 +266,7 @@ where } } -impl TryFrom for ClientQueryRequest +impl TryFrom for ClientQueryRequest where R: Query + Debug, >::Error: Into, @@ -276,14 +274,13 @@ where type Error = eyre::Report; fn try_from( - PaginatedQueryResult { + QueryResult { result, pagination, sorting, - total, - }: PaginatedQueryResult, + }: QueryResult, ) -> Result { - let output = R::Output::try_from(result.into()) + let output = R::Output::try_from(result) .map_err(Into::into) .wrap_err("Unexpected type")?; @@ -291,7 +288,6 @@ where output, pagination, sorting, - total, }) } } diff --git a/client/tests/integration/triggers/time_trigger.rs b/client/tests/integration/triggers/time_trigger.rs index d1b10b03a1e..9f8dcb0586e 100644 --- a/client/tests/integration/triggers/time_trigger.rs +++ b/client/tests/integration/triggers/time_trigger.rs @@ -118,10 +118,12 @@ fn change_asset_metadata_after_1_sec() -> Result<()> { usize::try_from(PERIOD_MS / DEFAULT_CONSENSUS_ESTIMATION_MS + 1)?, )?; - let value = test_client.request(FindAssetDefinitionKeyValueByIdAndKey { - id: asset_definition_id.into(), - key: key.into(), - })?; + let value = test_client + .request(FindAssetDefinitionKeyValueByIdAndKey { + id: asset_definition_id.into(), + key: key.into(), + }) + .map(Into::into)?; assert!(matches!(value, Value::Numeric(NumericValue::U32(3_u32)))); Ok(()) diff --git a/configs/peer/validator.wasm b/configs/peer/validator.wasm index eaa334e821707f8a8f2a1b82297bafb1efa962ae..c798e633616cf22df471058f29de82ca51e02940 100644 GIT binary patch delta 82493 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 delta 83224 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 Wa#hxx0T@FAHTp`LvoublkNrRIvtN$@ diff --git a/core/quang b/core/quang new file mode 100644 index 00000000000..0c85ddc7f6b --- /dev/null +++ b/core/quang @@ -0,0 +1,10 @@ +1. repositories: + - cbindgen(git@github.com:mozilla/cbindgen.git), + - iroha2(git@github.com:hyperledger/iroha.git) iroha2-stable branch + - iroha2-java(git@github.com:hyperledger/iroha-java.git) + +2. run iroha nodes and connect to it via iroha_client_cli and http app +3. run cbindgen on a small example: + - export a function from Rust + - generate C bindings with cbindgen + - link this C lib from C and Java diff --git a/core/src/kura.rs b/core/src/kura.rs index fd20f270592..93a6ba9e1df 100644 --- a/core/src/kura.rs +++ b/core/src/kura.rs @@ -319,17 +319,17 @@ impl Kura { } /// Put a block in kura's in memory block store. - pub fn store_block(&self, block: impl Into>) { - let block = block.into(); - self.block_data.lock().push((block.hash(), Some(block))); + pub fn store_block(&self, block: VersionedCommittedBlock) { + self.block_data + .lock() + .push((block.hash(), Some(Arc::new(block)))); } /// Replace the block in `Kura`'s in memory block store. - pub fn replace_top_block(&self, block: impl Into>) { - let block = block.into(); + pub fn replace_top_block(&self, block: VersionedCommittedBlock) { let mut data = self.block_data.lock(); data.pop(); - data.push((block.hash(), Some(block))); + data.push((block.hash(), Some(Arc::new(block)))); } } diff --git a/core/src/queue.rs b/core/src/queue.rs index 5a6d144b6f4..a6e25487141 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -175,12 +175,14 @@ impl Queue { } /// Returns all pending transactions. - pub fn all_transactions(&self, wsv: &WorldStateView) -> Vec { + pub fn all_transactions<'wsv>( + &'wsv self, + wsv: &'wsv WorldStateView, + ) -> impl Iterator + 'wsv { self.txs .iter() .filter(|e| self.is_pending(e.value(), wsv)) .map(|e| e.value().clone()) - .collect() } /// Returns `n` randomly selected transaction from the queue. diff --git a/core/src/smartcontracts/isi/account.rs b/core/src/smartcontracts/isi/account.rs index 0f5033eb5b3..3cda1ee2bcf 100644 --- a/core/src/smartcontracts/isi/account.rs +++ b/core/src/smartcontracts/isi/account.rs @@ -479,53 +479,65 @@ pub mod query { }; use super::*; + use crate::smartcontracts::query::Lazy; impl ValidQuery for FindRolesByAccountId { #[metrics(+"find_roles_by_account_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let account_id = wsv .evaluate(&self.id) .wrap_err("Failed to evaluate account id") .map_err(|e| Error::Evaluate(e.to_string()))?; iroha_logger::trace!(%account_id, roles=?wsv.world.roles); - let roles = wsv.map_account(&account_id, |account| { - account.roles.iter().cloned().collect::>() - })?; - Ok(roles) + Ok(Box::new( + wsv.map_account(&account_id, |account| &account.roles)? + .iter() + .cloned(), + )) } } impl ValidQuery for FindPermissionTokensByAccountId { #[metrics(+"find_permission_tokens_by_account_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let account_id = wsv .evaluate(&self.id) .wrap_err("Failed to evaluate account id") .map_err(|e| Error::Evaluate(e.to_string()))?; iroha_logger::trace!(%account_id, accounts=?wsv.world.domains); - let tokens = wsv.map_account(&account_id, |account| { - wsv.account_permission_tokens(account) - })?; - Ok(tokens.into_iter().collect()) + Ok(Box::new( + wsv.account_permission_tokens(&account_id)?.cloned(), + )) } } impl ValidQuery for FindAllAccounts { #[metrics(+"find_all_accounts")] - fn execute(&self, wsv: &WorldStateView) -> Result { - let mut vec = Vec::new(); - for domain in wsv.domains().values() { - for account in domain.accounts.values() { - vec.push(account.clone()) - } - } - Ok(vec) + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { + Ok(Box::new( + wsv.domains() + .values() + .flat_map(|domain| domain.accounts.values()) + .cloned(), + )) } } impl ValidQuery for FindAccountById { #[metrics(+"find_account_by_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.id) .wrap_err("Failed to get id") @@ -537,39 +549,53 @@ pub mod query { impl ValidQuery for FindAccountsByName { #[metrics(+"find_account_by_name")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let name = wsv .evaluate(&self.name) .wrap_err("Failed to get account name") .map_err(|e| Error::Evaluate(e.to_string()))?; iroha_logger::trace!(%name); - let mut vec = Vec::new(); - for domain in wsv.domains().values() { - for account in domain.accounts.values() { - if account.id().name == name { - vec.push(account.clone()) - } - } - } - Ok(vec) + Ok(Box::new( + wsv.domains() + .values() + .flat_map(move |domain| { + let name = name.clone(); + + domain + .accounts + .values() + .filter(move |account| account.id().name == name) + }) + .cloned(), + )) } } impl ValidQuery for FindAccountsByDomainId { #[metrics(+"find_accounts_by_domain_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.domain_id) .wrap_err("Failed to get domain id") .map_err(|e| Error::Evaluate(e.to_string()))?; + iroha_logger::trace!(%id); - Ok(wsv.domain(&id)?.accounts.values().cloned().collect()) + Ok(Box::new(wsv.domain(&id)?.accounts.values().cloned())) } } impl ValidQuery for FindAccountKeyValueByIdAndKey { #[metrics(+"find_account_key_value_by_id_and_key")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.id) .wrap_err("Failed to get account id") @@ -581,34 +607,32 @@ pub mod query { iroha_logger::trace!(%id, %key); wsv.map_account(&id, |account| account.metadata.get(&key).map(Clone::clone))? .ok_or_else(|| FindError::MetadataKey(key).into()) + .map(Into::into) } } impl ValidQuery for FindAccountsWithAsset { #[metrics(+"find_accounts_with_asset")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let asset_definition_id = wsv .evaluate(&self.asset_definition_id) .wrap_err("Failed to get asset id") .map_err(|e| Error::Evaluate(e.to_string()))?; iroha_logger::trace!(%asset_definition_id); - let domain_id = &asset_definition_id.domain_id; - - wsv.map_domain(domain_id, |domain| { - let found = domain - .accounts - .values() - .filter(|account| { + Ok(Box::new( + wsv.map_domain(&asset_definition_id.domain_id.clone(), move |domain| { + domain.accounts.values().filter(move |account| { let asset_id = AssetId::new(asset_definition_id.clone(), account.id().clone()); account.assets.get(&asset_id).is_some() }) - .cloned() - .collect(); - Ok(found) - }) - .map_err(Into::into) + })? + .cloned(), + )) } } } diff --git a/core/src/smartcontracts/isi/asset.rs b/core/src/smartcontracts/isi/asset.rs index d7ed6ba8aa6..3d6af0b4ef1 100644 --- a/core/src/smartcontracts/isi/asset.rs +++ b/core/src/smartcontracts/isi/asset.rs @@ -445,42 +445,49 @@ pub mod query { }; use super::*; + use crate::smartcontracts::query::Lazy; impl ValidQuery for FindAllAssets { #[metrics(+"find_all_assets")] - fn execute(&self, wsv: &WorldStateView) -> Result { - Ok(wsv - .domains() - .values() - .map(|domain| { - domain - .accounts - .values() - .map(|account| account.assets.values()) - .flatten() - }) - .flatten() - .cloned() - .collect()) + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { + Ok(Box::new( + wsv.domains() + .values() + .flat_map(|domain| { + domain + .accounts + .values() + .flat_map(|account| account.assets.values()) + }) + .cloned(), + )) } } impl ValidQuery for FindAllAssetsDefinitions { #[metrics(+"find_all_asset_definitions")] - fn execute(&self, wsv: &WorldStateView) -> Result { - let mut vec = Vec::new(); - for domain in wsv.domains().values() { - for asset_definition in domain.asset_definitions.values() { - vec.push(asset_definition.clone()) - } - } - Ok(vec) + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { + Ok(Box::new( + wsv.domains() + .values() + .flat_map(|domain| domain.asset_definitions.values()) + .cloned(), + )) } } impl ValidQuery for FindAssetById { #[metrics(+"find_asset_by_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.id) .wrap_err("Failed to get asset id") @@ -498,7 +505,10 @@ pub mod query { impl ValidQuery for FindAssetDefinitionById { #[metrics(+"find_asset_defintion_by_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.id) .wrap_err("Failed to get asset definition id") @@ -512,81 +522,108 @@ pub mod query { impl ValidQuery for FindAssetsByName { #[metrics(+"find_assets_by_name")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let name = wsv .evaluate(&self.name) .wrap_err("Failed to get asset name") .map_err(|e| Error::Evaluate(e.to_string()))?; iroha_logger::trace!(%name); - let mut vec = Vec::new(); - for domain in wsv.domains().values() { - for account in domain.accounts.values() { - for asset in account.assets.values() { - if asset.id().definition_id.name == name { - vec.push(asset.clone()) - } - } - } - } - Ok(vec) + Ok(Box::new( + wsv.domains() + .values() + .flat_map(move |domain| { + let name = name.clone(); + + domain.accounts.values().flat_map(move |account| { + let name = name.clone(); + + account + .assets + .values() + .filter(move |asset| asset.id().definition_id.name == name) + }) + }) + .cloned(), + )) } } impl ValidQuery for FindAssetsByAccountId { #[metrics(+"find_assets_by_account_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.account_id) .wrap_err("Failed to get account id") .map_err(|e| Error::Evaluate(e.to_string()))?; iroha_logger::trace!(%id); - wsv.account_assets(&id).map_err(Into::into) + Ok(Box::new(wsv.account_assets(&id)?)) } } impl ValidQuery for FindAssetsByAssetDefinitionId { #[metrics(+"find_assets_by_asset_definition_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.asset_definition_id) .wrap_err("Failed to get asset definition id") .map_err(|e| Error::Evaluate(e.to_string()))?; iroha_logger::trace!(%id); - let mut vec = Vec::new(); - for domain in wsv.domains().values() { - for account in domain.accounts.values() { - for asset in account.assets.values() { - if asset.id().definition_id == id { - vec.push(asset.clone()) - } - } - } - } - Ok(vec) + Ok(Box::new( + wsv.domains() + .values() + .flat_map(move |domain| { + let id = id.clone(); + + domain.accounts.values().flat_map(move |account| { + let id = id.clone(); + + account + .assets + .values() + .filter(move |asset| asset.id().definition_id == id) + }) + }) + .cloned(), + )) } } impl ValidQuery for FindAssetsByDomainId { #[metrics(+"find_assets_by_domain_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.domain_id) .wrap_err("Failed to get domain id") .map_err(|e| Error::Evaluate(e.to_string()))?; iroha_logger::trace!(%id); - let mut vec = Vec::new(); - for account in wsv.domain(&id)?.accounts.values() { - for asset in account.assets.values() { - vec.push(asset.clone()) - } - } - Ok(vec) + Ok(Box::new( + wsv.domain(&id)? + .accounts + .values() + .flat_map(|account| account.assets.values()) + .cloned(), + )) } } impl ValidQuery for FindAssetsByDomainIdAndAssetDefinitionId { #[metrics(+"find_assets_by_domain_id_and_asset_definition_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let domain_id = wsv .evaluate(&self.domain_id) .wrap_err("Failed to get domain id") @@ -601,23 +638,30 @@ pub mod query { .get(&asset_definition_id) .ok_or_else(|| FindError::AssetDefinition(asset_definition_id.clone()))?; iroha_logger::trace!(%domain_id, %asset_definition_id); - let mut assets = Vec::new(); - for account in domain.accounts.values() { - for asset in account.assets.values() { - if asset.id().account_id.domain_id == domain_id - && asset.id().definition_id == asset_definition_id - { - assets.push(asset.clone()) - } - } - } - Ok(assets) + Ok(Box::new( + domain + .accounts + .values() + .flat_map(move |account| { + let domain_id = domain_id.clone(); + let asset_definition_id = asset_definition_id.clone(); + + account.assets.values().filter(move |asset| { + asset.id().account_id.domain_id == domain_id + && asset.id().definition_id == asset_definition_id + }) + }) + .cloned(), + )) } } impl ValidQuery for FindAssetQuantityById { #[metrics(+"find_asset_quantity_by_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.id) .wrap_err("Failed to get asset id") @@ -641,7 +685,10 @@ pub mod query { impl ValidQuery for FindTotalAssetQuantityByAssetDefinitionId { #[metrics(+"find_total_asset_quantity_by_asset_definition_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.id) .wrap_err("Failed to get asset definition id") @@ -654,7 +701,10 @@ pub mod query { impl ValidQuery for FindAssetKeyValueByIdAndKey { #[metrics(+"find_asset_key_value_by_id_and_key")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.id) .wrap_err("Failed to get asset id") @@ -676,16 +726,20 @@ pub mod query { .try_as_ref() .map_err(eyre::Error::from) .map_err(|e| Error::Conversion(e.to_string()))?; - Ok(store + store .get(&key) - .ok_or_else(|| Error::Find(Box::new(FindError::MetadataKey(key))))? - .clone()) + .ok_or_else(|| Error::Find(Box::new(FindError::MetadataKey(key)))) + .cloned() + .map(Into::into) } } impl ValidQuery for IsAssetDefinitionOwner { #[metrics("is_asset_definition_owner")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let asset_definition_id = wsv .evaluate(&self.asset_definition_id) .wrap_err("Failed to get asset definition id") diff --git a/core/src/smartcontracts/isi/block.rs b/core/src/smartcontracts/isi/block.rs index 4cfd413c6bb..d70c95ad998 100644 --- a/core/src/smartcontracts/isi/block.rs +++ b/core/src/smartcontracts/isi/block.rs @@ -1,7 +1,6 @@ //! This module contains trait implementations related to block queries use eyre::{Result, WrapErr}; use iroha_data_model::{ - block::VersionedCommittedBlock, evaluate::ExpressionEvaluator, query::{ block::FindBlockHeaderByHash, @@ -11,34 +10,40 @@ use iroha_data_model::{ use iroha_telemetry::metrics; use super::*; +use crate::smartcontracts::query::Lazy; impl ValidQuery for FindAllBlocks { #[metrics(+"find_all_blocks")] - fn execute(&self, wsv: &WorldStateView) -> Result { - let blocks = wsv - .all_blocks() - .map(|block| VersionedCommittedBlock::clone(&block)) - .rev() - .collect(); - Ok(blocks) + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, QueryExecutionFail> { + Ok(Box::new( + wsv.all_blocks().rev().map(|block| Clone::clone(&*block)), + )) } } impl ValidQuery for FindAllBlockHeaders { #[metrics(+"find_all_block_headers")] - fn execute(&self, wsv: &WorldStateView) -> Result { - let block_headers = wsv - .all_blocks() - .rev() - .map(|block| block.as_v1().header.clone()) - .collect(); - Ok(block_headers) + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, QueryExecutionFail> { + Ok(Box::new( + wsv.all_blocks() + .rev() + .map(|block| block.as_v1().header.clone()), + )) } } impl ValidQuery for FindBlockHeaderByHash { #[metrics(+"find_block_header")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, QueryExecutionFail> { let hash = wsv .evaluate(&self.hash) .wrap_err("Failed to evaluate hash") diff --git a/core/src/smartcontracts/isi/domain.rs b/core/src/smartcontracts/isi/domain.rs index 177bca4419c..e208457a9dc 100644 --- a/core/src/smartcontracts/isi/domain.rs +++ b/core/src/smartcontracts/isi/domain.rs @@ -291,17 +291,24 @@ pub mod query { use iroha_data_model::query::error::QueryExecutionFail as Error; use super::*; + use crate::smartcontracts::query::Lazy; impl ValidQuery for FindAllDomains { #[metrics(+"find_all_domains")] - fn execute(&self, wsv: &WorldStateView) -> Result { - Ok(wsv.domains().values().cloned().collect()) + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { + Ok(Box::new(wsv.domains().values().cloned())) } } impl ValidQuery for FindDomainById { #[metrics(+"find_domain_by_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.id) .wrap_err("Failed to get domain id") @@ -313,7 +320,10 @@ pub mod query { impl ValidQuery for FindDomainKeyValueByIdAndKey { #[metrics(+"find_domain_key_value_by_id_and_key")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.id) .wrap_err("Failed to get domain id") @@ -323,16 +333,18 @@ pub mod query { .wrap_err("Failed to get key") .map_err(|e| Error::Evaluate(e.to_string()))?; iroha_logger::trace!(%id, %key); - wsv.map_domain(&id, |domain| { - Ok(domain.metadata.get(&key).map(Clone::clone)) - })? - .ok_or_else(|| FindError::MetadataKey(key).into()) + wsv.map_domain(&id, |domain| domain.metadata.get(&key).map(Clone::clone))? + .ok_or_else(|| FindError::MetadataKey(key).into()) + .map(Into::into) } } impl ValidQuery for FindAssetDefinitionKeyValueByIdAndKey { #[metrics(+"find_asset_definition_key_value_by_id_and_key")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.id) .wrap_err("Failed to get asset definition id") @@ -346,8 +358,9 @@ pub mod query { .asset_definition(&id)? .metadata .get(&key) - .ok_or(FindError::MetadataKey(key))? - .clone()) + .ok_or(FindError::MetadataKey(key)) + .cloned() + .map(Into::into)?) } } } diff --git a/core/src/smartcontracts/isi/query.rs b/core/src/smartcontracts/isi/query.rs index c03fc6d2196..201f9b31922 100644 --- a/core/src/smartcontracts/isi/query.rs +++ b/core/src/smartcontracts/isi/query.rs @@ -11,6 +11,49 @@ use parity_scale_codec::{Decode, Encode}; use crate::{prelude::ValidQuery, WorldStateView}; +/// Represents lazy evaluated query output +pub trait Lazy { + /// Type of the lazy evaluated query output + type Lazy<'a>; +} + +/// Lazily evaluated equivalent of [`Value`] +pub enum LazyValue<'a> { + /// Concrete computed [`Value`] + Value(Value), + /// Iterator over a set of [`Value`]s + Iter(Box + 'a>), +} + +impl Lazy for Value { + type Lazy<'a> = LazyValue<'a>; +} + +impl Lazy for Vec { + type Lazy<'a> = Box + 'a>; +} + +macro_rules! impl_lazy { + ( $($ident:ty),+ $(,)? ) => { $( + impl Lazy for $ident { + type Lazy<'a> = Self; + } )+ + }; +} +impl_lazy! { + bool, + NumericValue, + iroha_data_model::role::Role, + iroha_data_model::asset::Asset, + iroha_data_model::asset::AssetDefinition, + iroha_data_model::account::Account, + iroha_data_model::domain::Domain, + iroha_data_model::block::BlockHeader, + iroha_data_model::query::MetadataValue, + iroha_data_model::query::TransactionQueryResult, + iroha_data_model::trigger::Trigger, +} + /// Query Request statefully validated on the Iroha node side. #[derive(Debug, Decode, Encode)] pub struct ValidQueryRequest(VersionedSignedQuery); @@ -47,66 +90,86 @@ impl ValidQueryRequest { /// /// # Errors /// Forwards `self.query.execute` error. - #[inline] - pub fn execute(&self, wsv: &WorldStateView) -> Result { - Ok(self.0.filter().filter(self.0.query().execute(wsv)?)) + pub fn execute<'wsv>(&'wsv self, wsv: &'wsv WorldStateView) -> Result, Error> { + let value = self.0.query().execute(wsv)?; + + Ok(if let LazyValue::Iter(iter) = value { + LazyValue::Iter(Box::new(iter.filter(|val| self.0.filter().applies(val)))) + } else { + value + }) + + // We're not handling the LimitedMetadata case, because + // the predicate when applied to it is ambiguous. We could + // pattern match on that case, but we should assume that + // metadata (since it's limited) isn't going to be too + // difficult to filter client-side. I actually think that + // Metadata should be restricted in what types it can + // contain. } } impl ValidQuery for QueryBox { - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { iroha_logger::debug!(query=%self, "Executing"); macro_rules! match_all { - ( $( $query:ident ),+ $(,)? ) => { + ( non_iter: {$( $non_iter_query:ident ),+ $(,)?} $( $query:ident, )+ ) => { match self { $( - QueryBox::$query(query) => query.execute(wsv).map(Into::into), )+ + QueryBox::$non_iter_query(query) => query.execute(wsv).map(Value::from).map(LazyValue::Value), )+ $( + QueryBox::$query(query) => query.execute(wsv).map(|i| i.map(Value::from)).map(|iter| LazyValue::Iter(Box::new(iter))), )+ } }; } match_all! { + non_iter: { + FindAccountById, + FindAssetById, + FindAssetDefinitionById, + FindAssetQuantityById, + FindTotalAssetQuantityByAssetDefinitionId, + IsAssetDefinitionOwner, + FindDomainById, + FindBlockHeaderByHash, + FindTransactionByHash, + DoesAccountHavePermissionToken, + FindTriggerById, + FindRoleByRoleId, + FindDomainKeyValueByIdAndKey, + FindAssetKeyValueByIdAndKey, + FindAccountKeyValueByIdAndKey, + FindAssetDefinitionKeyValueByIdAndKey, + FindTriggerKeyValueByIdAndKey, + } + FindAllAccounts, - FindAccountById, FindAccountsByName, FindAccountsByDomainId, FindAccountsWithAsset, FindAllAssets, FindAllAssetsDefinitions, - FindAssetById, - FindAssetDefinitionById, FindAssetsByName, FindAssetsByAccountId, FindAssetsByAssetDefinitionId, FindAssetsByDomainId, FindAssetsByDomainIdAndAssetDefinitionId, - FindAssetQuantityById, - FindTotalAssetQuantityByAssetDefinitionId, - IsAssetDefinitionOwner, FindAllDomains, - FindDomainById, - FindDomainKeyValueByIdAndKey, FindAllPeers, - FindAssetKeyValueByIdAndKey, - FindAccountKeyValueByIdAndKey, FindAllBlocks, FindAllBlockHeaders, - FindBlockHeaderByHash, FindAllTransactions, FindTransactionsByAccountId, - FindTransactionByHash, FindPermissionTokensByAccountId, FindAllPermissionTokenDefinitions, - DoesAccountHavePermissionToken, - FindAssetDefinitionKeyValueByIdAndKey, FindAllActiveTriggerIds, - FindTriggerById, - FindTriggerKeyValueByIdAndKey, FindTriggersByDomainId, FindAllRoles, FindAllRoleIds, FindRolesByAccountId, - FindRoleByRoleId, FindAllParameters, } } @@ -275,8 +338,8 @@ mod tests { let bytes = FindAssetKeyValueByIdAndKey::new(asset_id, Name::from_str("Bytes")?).execute(&wsv)?; assert_eq!( - bytes, - Value::Vec(vec![1_u32.to_value(), 2_u32.to_value(), 3_u32.to_value()]) + Value::Vec(vec![1_u32.to_value(), 2_u32.to_value(), 3_u32.to_value()]), + bytes.into(), ); Ok(()) } @@ -289,8 +352,8 @@ mod tests { let bytes = FindAccountKeyValueByIdAndKey::new(ALICE_ID.clone(), Name::from_str("Bytes")?) .execute(&wsv)?; assert_eq!( - bytes, - Value::Vec(vec![1_u32.to_value(), 2_u32.to_value(), 3_u32.to_value()]) + Value::Vec(vec![1_u32.to_value(), 2_u32.to_value(), 3_u32.to_value()]), + bytes.into(), ); Ok(()) } @@ -300,8 +363,7 @@ mod tests { let num_blocks = 100; let wsv = wsv_with_test_blocks_and_transactions(num_blocks, 1, 1)?; - - let blocks = FindAllBlocks.execute(&wsv)?; + let blocks = FindAllBlocks.execute(&wsv)?.collect::>(); assert_eq!(blocks.len() as u64, num_blocks); assert!(blocks.windows(2).all(|wnd| wnd[0] >= wnd[1])); @@ -314,8 +376,7 @@ mod tests { let num_blocks = 100; let wsv = wsv_with_test_blocks_and_transactions(num_blocks, 1, 1)?; - - let block_headers = FindAllBlockHeaders.execute(&wsv)?; + let block_headers = FindAllBlockHeaders.execute(&wsv)?.collect::>(); assert_eq!(block_headers.len() as u64, num_blocks); assert!(block_headers.windows(2).all(|wnd| wnd[0] >= wnd[1])); @@ -347,8 +408,7 @@ mod tests { let num_blocks = 100; let wsv = wsv_with_test_blocks_and_transactions(num_blocks, 1, 1)?; - - let txs = FindAllTransactions.execute(&wsv)?; + let txs = FindAllTransactions.execute(&wsv)?.collect::>(); assert_eq!(txs.len() as u64, num_blocks * 2); assert_eq!( @@ -444,8 +504,8 @@ mod tests { let key = Name::from_str("Bytes")?; let bytes = FindDomainKeyValueByIdAndKey::new(domain_id, key).execute(&wsv)?; assert_eq!( - bytes, - Value::Vec(vec![1_u32.to_value(), 2_u32.to_value(), 3_u32.to_value()]) + Value::Vec(vec![1_u32.to_value(), 2_u32.to_value(), 3_u32.to_value()]), + bytes.into(), ); Ok(()) } diff --git a/core/src/smartcontracts/isi/triggers/mod.rs b/core/src/smartcontracts/isi/triggers/mod.rs index 384a4ed076f..6e2e9613ca9 100644 --- a/core/src/smartcontracts/isi/triggers/mod.rs +++ b/core/src/smartcontracts/isi/triggers/mod.rs @@ -198,18 +198,24 @@ pub mod query { use iroha_data_model::query::error::QueryExecutionFail as Error; use super::*; - use crate::prelude::*; + use crate::{prelude::*, smartcontracts::query::Lazy}; impl ValidQuery for FindAllActiveTriggerIds { #[metrics(+"find_all_active_triggers")] - fn execute(&self, wsv: &WorldStateView) -> Result { - Ok(wsv.triggers().ids()) + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { + Ok(Box::new(wsv.triggers().ids().cloned())) } } impl ValidQuery for FindTriggerById { #[metrics(+"find_trigger_by_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.id) .map_err(|e| Error::Evaluate(format!("Failed to evaluate trigger id. {e}")))?; @@ -237,7 +243,10 @@ pub mod query { impl ValidQuery for FindTriggerKeyValueByIdAndKey { #[metrics(+"find_trigger_key_value_by_id_and_key")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let id = wsv .evaluate(&self.id) .map_err(|e| Error::Evaluate(format!("Failed to evaluate trigger id. {e}")))?; @@ -250,23 +259,27 @@ pub mod query { action .metadata() .get(&key) - .map(Clone::clone) + .cloned() .ok_or_else(|| FindError::MetadataKey(key.clone()).into()) }) .ok_or_else(|| Error::Find(Box::new(FindError::Trigger(id))))? + .map(Into::into) } } impl ValidQuery for FindTriggersByDomainId { #[metrics(+"find_triggers_by_domain_id")] - fn execute(&self, wsv: &WorldStateView) -> eyre::Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> eyre::Result<::Lazy<'wsv>, Error> { let domain_id = wsv .evaluate(&self.domain_id) .map_err(|e| Error::Evaluate(format!("Failed to evaluate domain id. {e}")))?; - let triggers = wsv - .triggers() - .inspect_by_domain_id(&domain_id, |trigger_id, action| { + Ok(Box::new(wsv.triggers().inspect_by_domain_id( + &domain_id, + |trigger_id, action| { let Action { executable: loaded_executable, repeats, @@ -274,14 +287,14 @@ pub mod query { filter, metadata, } = action.clone_and_box(); + Trigger::new( trigger_id.clone(), Action::new(loaded_executable, repeats, authority, filter) .with_metadata(metadata), ) - }); - - Ok(triggers) + }, + ))) } } } diff --git a/core/src/smartcontracts/isi/triggers/set.rs b/core/src/smartcontracts/isi/triggers/set.rs index f30115715a1..ba0a26c50ea 100644 --- a/core/src/smartcontracts/isi/triggers/set.rs +++ b/core/src/smartcontracts/isi/triggers/set.rs @@ -202,14 +202,18 @@ impl Set { /// Get all contained trigger ids without a particular order #[inline] - pub fn ids(&self) -> Vec { - self.ids.keys().cloned().collect() + pub fn ids(&self) -> impl ExactSizeIterator { + self.ids.keys() } /// Apply `f` to triggers that belong to the given [`DomainId`] /// /// Return an empty list if [`Set`] doesn't contain any triggers belonging to [`DomainId`]. - pub fn inspect_by_domain_id(&self, domain_id: &DomainId, f: F) -> Vec + pub fn inspect_by_domain_id( + &self, + domain_id: &DomainId, + f: F, + ) -> impl ExactSizeIterator where F: Fn(&TriggerId, &dyn ActionTrait) -> R, { @@ -251,7 +255,8 @@ impl Set { Some(result) }) - .collect() + .collect::>() + .into_iter() } /// Apply `f` to the trigger identified by `id`. diff --git a/core/src/smartcontracts/isi/tx.rs b/core/src/smartcontracts/isi/tx.rs index d1531220d8d..a92e9505a38 100644 --- a/core/src/smartcontracts/isi/tx.rs +++ b/core/src/smartcontracts/isi/tx.rs @@ -1,48 +1,157 @@ //! Query module provides [`Query`] Transaction related implementations. +use std::sync::Arc; + use eyre::{Result, WrapErr}; +use iroha_crypto::HashOf; use iroha_data_model::{ + block::VersionedCommittedBlock, evaluate::ExpressionEvaluator, prelude::*, - query::error::{FindError, QueryExecutionFail}, + query::{ + error::{FindError, QueryExecutionFail}, + {TransactionQueryResult, TransactionValue}, + }, }; use iroha_telemetry::metrics; -use super::*; +use super::{query::Lazy, *}; + +pub(crate) struct BlockTransactionIter(Arc, usize); +pub(crate) struct BlockTransactionRef(Arc, usize); + +impl BlockTransactionIter { + fn new(block: Arc) -> Self { + Self(block, 0) + } +} + +impl Iterator for BlockTransactionIter { + type Item = BlockTransactionRef; + + fn next(&mut self) -> Option { + let block = self.0.as_v1(); + + if self.1 < block.rejected_transactions.len() { + return Some(BlockTransactionRef(Arc::clone(&self.0), self.1)); + } + + None + } +} + +impl BlockTransactionRef { + fn block_hash(&self) -> HashOf { + self.0.hash() + } + + fn authority(&self) -> &AccountId { + let block = self.0.as_v1(); + + if self.1 < block.transactions.len() { + return &block.transactions[self.1].payload().authority; + } + + &block.rejected_transactions[self.1] + .transaction + .payload() + .authority + } + fn value(&self) -> TransactionValue { + let block = self.0.as_v1(); + + if self.1 < block.transactions.len() { + return TransactionValue::Transaction(block.transactions[self.1].clone()); + } + + TransactionValue::RejectedTransaction(block.rejected_transactions[self.1].clone()) + } +} impl ValidQuery for FindAllTransactions { #[metrics(+"find_all_transactions")] - fn execute(&self, wsv: &WorldStateView) -> Result { - let mut txs = wsv.transaction_values(); - txs.reverse(); - Ok(txs) + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, QueryExecutionFail> { + Ok(Box::new( + wsv.all_blocks() + .flat_map(BlockTransactionIter::new) + .map(|tx| TransactionQueryResult { + block_hash: tx.block_hash(), + transaction: tx.value(), + }), + )) } } impl ValidQuery for FindTransactionsByAccountId { #[metrics(+"find_transactions_by_account_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { - let id = wsv + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, QueryExecutionFail> { + let account_id = wsv .evaluate(&self.account_id) .wrap_err("Failed to get account id") .map_err(|e| QueryExecutionFail::Evaluate(e.to_string()))?; - iroha_logger::trace!(%id); - Ok(wsv.transactions_values_by_account_id(&id)) + + Ok(Box::new( + wsv.all_blocks() + .flat_map(BlockTransactionIter::new) + .filter(move |tx| *tx.authority() == account_id) + .map(|tx| TransactionQueryResult { + block_hash: tx.block_hash(), + transaction: tx.value(), + }), + )) } } impl ValidQuery for FindTransactionByHash { #[metrics(+"find_transaction_by_hash")] - fn execute(&self, wsv: &WorldStateView) -> Result { - let hash = wsv + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, QueryExecutionFail> { + let tx_hash = wsv .evaluate(&self.hash) .wrap_err("Failed to get hash") .map_err(|e| QueryExecutionFail::Evaluate(e.to_string()))?; - iroha_logger::trace!(%hash); - if !wsv.has_transaction(hash) { - return Err(FindError::Transaction(hash).into()); + iroha_logger::trace!(%tx_hash); + if !wsv.has_transaction(tx_hash) { + return Err(FindError::Transaction(tx_hash).into()); }; - wsv.transaction_value_by_hash(&hash) - .ok_or_else(|| FindError::Transaction(hash).into()) + let block = wsv + .block_with_tx(&tx_hash) + .ok_or_else(|| FindError::Transaction(tx_hash))?; + + let block_hash = block.hash(); + let block = block.as_v1(); + + block + .transactions + .iter() + .find(|transaction| transaction.hash() == tx_hash) + .cloned() + .map(TransactionValue::Transaction) + .or_else(|| { + block + .rejected_transactions + .iter() + .find( + |RejectedTransaction { + transaction, + error: _, + }| transaction.hash() == tx_hash, + ) + .cloned() + .map(TransactionValue::RejectedTransaction) + }) + .map(|transaction| TransactionQueryResult { + block_hash, + transaction, + }) + .ok_or_else(|| FindError::Transaction(tx_hash).into()) } } diff --git a/core/src/smartcontracts/isi/world.rs b/core/src/smartcontracts/isi/world.rs index ebcc12b8fbd..4cae609c2ac 100644 --- a/core/src/smartcontracts/isi/world.rs +++ b/core/src/smartcontracts/isi/world.rs @@ -22,6 +22,7 @@ pub mod isi { use eyre::Result; use iroha_data_model::{ isi::error::{InvalidParameterError, RepetitionError}, + permission::PermissionTokenId, prelude::*, query::error::FindError, }; @@ -267,21 +268,24 @@ pub mod isi { /// Remove all tokens with specified definition id from all accounts in all domains fn remove_token_from_accounts( wsv: &mut WorldStateView, - target_definition_id: &::Id, + target_definition_id: &PermissionTokenId, ) -> Result<(), Error> { let mut accounts_with_token = std::collections::HashMap::new(); - for domain in wsv.domains().values() { - let account_ids = domain.accounts.values().map(|account| { - ( - account.id().clone(), - wsv.account_inherent_permission_tokens(account) - .filter(|token| token.definition_id == *target_definition_id) - .collect::>(), - ) - }); - - accounts_with_token.extend(account_ids); + let account_ids = wsv + .domains() + .values() + .flat_map(|domain| domain.accounts.values()) + .map(|account| &account.id); + + for account_id in account_ids { + accounts_with_token.insert( + account_id.clone(), + wsv.account_inherent_permission_tokens(account_id)? + .filter(|token| token.definition_id == *target_definition_id) + .cloned() + .collect::>(), + ); } let mut events = Vec::new(); @@ -413,31 +417,40 @@ pub mod query { }; use super::*; + use crate::smartcontracts::query::Lazy; impl ValidQuery for FindAllRoles { #[metrics(+"find_all_roles")] - fn execute(&self, wsv: &WorldStateView) -> Result { - Ok(wsv.world.roles.values().cloned().collect()) + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { + Ok(Box::new(wsv.world.roles.values().cloned())) } } impl ValidQuery for FindAllRoleIds { #[metrics(+"find_all_role_ids")] - fn execute(&self, wsv: &WorldStateView) -> Result { - Ok(wsv + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { + Ok(Box::new(wsv .world .roles .values() // To me, this should probably be a method, not a field. .map(Role::id) - .cloned() - .collect()) + .cloned())) } } impl ValidQuery for FindRoleByRoleId { #[metrics(+"find_role_by_role_id")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let role_id = wsv .evaluate(&self.id) .map_err(|e| Error::Evaluate(e.to_string()))?; @@ -452,40 +465,49 @@ pub mod query { impl ValidQuery for FindAllPeers { #[metrics("find_all_peers")] - fn execute(&self, wsv: &WorldStateView) -> Result { - Ok(wsv.peers()) + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { + Ok(Box::new(wsv.peers().cloned().map(Peer::new))) } } impl ValidQuery for FindAllPermissionTokenDefinitions { #[metrics("find_all_token_ids")] - fn execute(&self, wsv: &WorldStateView) -> Result { - Ok(wsv - .permission_token_definitions() - .values() - .cloned() - .collect()) + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { + Ok(Box::new( + wsv.permission_token_definitions().values().cloned(), + )) } } impl ValidQuery for FindAllParameters { #[metrics("find_all_parameters")] - fn execute(&self, wsv: &WorldStateView) -> Result { - Ok(wsv.parameters()) + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { + Ok(Box::new(wsv.parameters().cloned())) } } impl ValidQuery for DoesAccountHavePermissionToken { #[metrics("does_account_have_permission")] - fn execute(&self, wsv: &WorldStateView) -> Result { + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, Error> { let authority = wsv .evaluate(&self.account_id) .map_err(|e| Error::Evaluate(e.to_string()))?; - wsv.map_account(&authority, |account| { - wsv.account_permission_tokens(account) - .contains(&self.permission_token) - }) + Ok(wsv + .account_permission_tokens(&authority)? + .any(|permission_token| *permission_token == self.permission_token)) } } } diff --git a/core/src/smartcontracts/mod.rs b/core/src/smartcontracts/mod.rs index 6580b1c766a..ebab9f86afe 100644 --- a/core/src/smartcontracts/mod.rs +++ b/core/src/smartcontracts/mod.rs @@ -15,6 +15,7 @@ use iroha_data_model::{ }; pub use isi::*; +use self::query::{Lazy, LazyValue}; use crate::wsv::WorldStateView; /// Trait implementations should provide actions to apply changes on [`WorldStateView`]. @@ -27,7 +28,10 @@ pub trait Execute { } /// This trait should be implemented for all Iroha Queries. -pub trait ValidQuery: Query { +pub trait ValidQuery: Query +where + Self::Output: Lazy, +{ /// Execute query on the [`WorldStateView`]. /// Should not mutate [`WorldStateView`]! /// @@ -35,7 +39,10 @@ pub trait ValidQuery: Query { /// /// # Errors /// Concrete to each implementer - fn execute(&self, wsv: &WorldStateView) -> Result; + fn execute<'wsv>( + &self, + wsv: &'wsv WorldStateView, + ) -> Result<::Lazy<'wsv>, QueryExecutionFail>; } impl ExpressionEvaluator for WorldStateView { @@ -65,7 +72,15 @@ impl<'a> Context<'a> { impl iroha_data_model::evaluate::Context for Context<'_> { fn query(&self, query: &QueryBox) -> Result { - query.execute(self.wsv).map_err(Into::into) + query + .execute(self.wsv) + .map(|value| match value { + LazyValue::Value(value) => value, + // NOTE: This will only be executed from the validator/executor. + // Handing out references to the host system is a security risk + LazyValue::Iter(iter) => Value::Vec(iter.collect()), + }) + .map_err(Into::into) } fn get(&self, name: &Name) -> Option<&Value> { diff --git a/core/src/smartcontracts/wasm.rs b/core/src/smartcontracts/wasm.rs index 7fde883f130..a4d71833dc4 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -25,6 +25,7 @@ use wasmtime::{ Caller, Config, Engine, Linker, Module, Store, StoreLimits, StoreLimitsBuilder, Trap, TypedFunc, }; +use super::query::LazyValue; use crate::{ smartcontracts::{Execute, ValidQuery as _}, wsv::WorldStateView, @@ -697,7 +698,14 @@ impl<'wrld, S: state::GetCommon<'wrld>, R: DefaultExecute> ExecuteOperations<'wr .clone() // Cloning validator is a cheap operation .validate(wsv, &common_state.authority, query.clone())?; - query.execute(wsv).map_err(Into::into) + query + .execute(wsv) + .map_err(Into::into) + .map(|lazy_value| match lazy_value { + LazyValue::Value(value) => value, + // NOTE: Returning references to the host system is a security risk + LazyValue::Iter(iter) => Value::Vec(iter.collect::>()), + }) } /// Default implementation of [`execute_instruction()`] @@ -872,7 +880,14 @@ impl<'wrld> ExecuteOperations<'wrld, state::Validator<'wrld>> for Runtime Result { iroha_logger::debug!(%query, "Executing as validator"); - query.execute(state.common_mut().wsv).map_err(Into::into) + query + .execute(state.common_mut().wsv) + .map_err(Into::into) + .map(|lazy_value| match lazy_value { + LazyValue::Value(value) => value, + // NOTE: Returning references to the host system is a security risk + LazyValue::Iter(iter) => Value::Vec(iter.collect::>()), + }) } #[codec::wrap] diff --git a/core/src/sumeragi/main_loop.rs b/core/src/sumeragi/main_loop.rs index 34cbf049b89..7396cda75f5 100644 --- a/core/src/sumeragi/main_loop.rs +++ b/core/src/sumeragi/main_loop.rs @@ -2,7 +2,7 @@ #![allow(clippy::cognitive_complexity)] use std::sync::mpsc; -use iroha_data_model::{block::*, transaction::error::TransactionRejectionReason}; +use iroha_data_model::{block::*, peer::PeerId, transaction::error::TransactionRejectionReason}; use iroha_p2p::UpdateTopology; use tracing::{span, Level}; @@ -291,17 +291,11 @@ impl Sumeragi { self.update_state::(block); } - fn update_topology(&mut self, committed_block: &VersionedCommittedBlock) { - let mut topology = Topology::new( - committed_block - .as_v1() - .header() - .committed_with_topology - .clone(), - ); + fn update_topology(&mut self, block_signees: &[PublicKey], peers: Vec) { + let mut topology = Topology::new(peers); topology.update_topology( - committed_block, + block_signees, self.wsv.peers_ids().iter().cloned().collect(), ); @@ -313,7 +307,7 @@ impl Sumeragi { &mut self, block: impl Into, ) { - let committed_block = Arc::new(block.into()); + let committed_block = block.into(); info!( addr=%self.peer_id.address, @@ -326,7 +320,7 @@ impl Sumeragi { Strategy::before_update_hook(self); self.wsv - .apply(committed_block.as_ref()) + .apply(&committed_block) .expect("Failed to apply block on WSV. Bailing."); let events_buffer = core::mem::take(&mut self.wsv.events_buffer); @@ -335,10 +329,23 @@ impl Sumeragi { // Parameters are updated before updating public copy of sumeragi self.update_params(); + let events: Vec<_> = (&committed_block).into(); + let topology = committed_block + .as_v1() + .header() + .committed_with_topology + .clone(); + let block_signees = committed_block + .signatures() + .map(|s| s.public_key()) + .cloned() + .collect::>(); + // https://github.com/hyperledger/iroha/issues/3396 // Kura should store the block only upon successful application to the internal WSV to avoid storing a corrupted block. // Public-facing WSV update should happen after that and be followed by `BlockCommited` event to prevent client access to uncommitted data. - Strategy::kura_store_block(&self.kura, Arc::clone(&committed_block)); + // TODO: Redundant clone + Strategy::kura_store_block(&self.kura, committed_block); // Update WSV copy that is public facing self.public_wsv_sender @@ -346,9 +353,9 @@ impl Sumeragi { // This sends "Block committed" event, so it should be done // AFTER public facing WSV update - self.send_events(committed_block.as_ref()); + self.send_events(events); - self.update_topology(committed_block.as_ref()); + self.update_topology(&block_signees, topology); self.cache_transaction() } @@ -1049,7 +1056,7 @@ trait ApplyBlockStrategy { fn before_update_hook(sumeragi: &mut Sumeragi); /// Operation to invoke in kura to store block. - fn kura_store_block(kura: &Kura, block: Arc); + fn kura_store_block(kura: &Kura, block: VersionedCommittedBlock); } /// Commit new block strategy. Used during normal consensus rounds. @@ -1065,7 +1072,7 @@ impl ApplyBlockStrategy for NewBlockStrategy { } #[inline] - fn kura_store_block(kura: &Kura, block: Arc) { + fn kura_store_block(kura: &Kura, block: VersionedCommittedBlock) { kura.store_block(block) } } @@ -1083,7 +1090,7 @@ impl ApplyBlockStrategy for ReplaceTopBlockStrategy { } #[inline] - fn kura_store_block(kura: &Kura, block: Arc) { + fn kura_store_block(kura: &Kura, block: VersionedCommittedBlock) { kura.replace_top_block(block) } } diff --git a/core/src/sumeragi/network_topology.rs b/core/src/sumeragi/network_topology.rs index d7a45eda694..9ed33183879 100644 --- a/core/src/sumeragi/network_topology.rs +++ b/core/src/sumeragi/network_topology.rs @@ -184,14 +184,8 @@ impl Topology { } /// Perform sequence of actions after block committed. - pub fn update_topology(&mut self, block: &VersionedCommittedBlock, new_peers: HashSet) { - self.lift_up_peers( - &block - .signatures() - .map(|s| s.public_key()) - .cloned() - .collect::>(), - ); + pub fn update_topology(&mut self, block_signees: &[PublicKey], new_peers: HashSet) { + self.lift_up_peers(block_signees); self.rotate_set_a(); self.update_peer_list(new_peers); } @@ -203,8 +197,13 @@ impl Topology { new_peers: HashSet, ) -> Self { let mut topology = Topology::new(block.as_v1().header().committed_with_topology.clone()); + let block_signees = block + .signatures() + .map(|s| s.public_key()) + .cloned() + .collect::>(); - topology.update_topology(block, new_peers); + topology.update_topology(&block_signees, new_peers); // Rotate all once for every view_change topology.rotate_all_n(view_change_index); diff --git a/core/src/wsv.rs b/core/src/wsv.rs index c069372fe28..9ac9ee7aa1e 100644 --- a/core/src/wsv.rs +++ b/core/src/wsv.rs @@ -10,7 +10,6 @@ use std::{ borrow::Borrow, collections::{BTreeSet, HashMap}, - convert::Infallible, fmt::Debug, sync::Arc, time::Duration, @@ -27,10 +26,7 @@ use iroha_data_model::{ isi::error::{InstructionExecutionError as Error, MathError}, parameter::Parameter, prelude::*, - query::{ - error::{FindError, QueryExecutionFail}, - TransactionQueryResult, - }, + query::error::{FindError, QueryExecutionFail}, trigger::action::ActionTrait, }; use iroha_logger::prelude::*; @@ -159,39 +155,58 @@ impl WorldStateView { /// /// # Errors /// Fails if there is no domain or account - pub fn account_assets(&self, id: &AccountId) -> Result, QueryExecutionFail> { - self.map_account(id, |account| account.assets.values().cloned().collect()) + pub fn account_assets( + &self, + id: &AccountId, + ) -> Result + '_, QueryExecutionFail> { + self.map_account(id, |account| account.assets.values().cloned()) } /// Return a set of all permission tokens granted to this account. - pub fn account_permission_tokens(&self, account: &Account) -> BTreeSet { - let mut tokens: BTreeSet = - self.account_inherent_permission_tokens(account).collect(); + /// + /// # Errors + /// + /// - if there is no account + pub fn account_permission_tokens( + &self, + account_id: &AccountId, + ) -> Result, FindError> { + let account = self.account(account_id)?; + + let mut tokens = self + .account_inherent_permission_tokens(account_id)? + .collect::>(); + for role_id in &account.roles { if let Some(role) = self.world.roles.get(role_id) { - tokens.append(&mut role.permissions.clone()); + tokens.extend(role.permissions.iter()); } } - tokens + + Ok(tokens.into_iter()) } /// Return a set of permission tokens granted to this account not as part of any role. + /// + /// # Errors + /// + /// - if there is no account pub fn account_inherent_permission_tokens( &self, - account: &Account, - ) -> impl ExactSizeIterator { + account_id: &AccountId, + ) -> Result, FindError> { self.world .account_permission_tokens - .get(&account.id) - .map_or_else(Default::default, Clone::clone) - .into_iter() + .get(account_id) + .ok_or_else(|| FindError::Account(account_id.clone())) + .map(std::collections::BTreeSet::iter) } /// Return `true` if [`Account`] contains a permission token not associated with any role. #[inline] pub fn account_contains_inherent_permission( &self, - account: &::Id, + account: &AccountId, token: &PermissionToken, ) -> bool { self.world @@ -203,11 +218,7 @@ impl WorldStateView { /// Add [`permission`](PermissionToken) to the [`Account`] if the account does not have this permission yet. /// /// Return a Boolean value indicating whether or not the [`Account`] already had this permission. - pub fn add_account_permission( - &mut self, - account: &::Id, - token: PermissionToken, - ) -> bool { + pub fn add_account_permission(&mut self, account: &AccountId, token: PermissionToken) -> bool { // `match` here instead of `map_or_else` to avoid cloning token into each closure match self.world.account_permission_tokens.get_mut(account) { None => { @@ -230,7 +241,7 @@ impl WorldStateView { /// Return a Boolean value indicating whether the [`Account`] had this permission. pub fn remove_account_permission( &mut self, - account: &::Id, + account: &AccountId, token: &PermissionToken, ) -> bool { self.world @@ -455,7 +466,7 @@ impl WorldStateView { /// - No such [`Asset`] /// - The [`Account`] with which the [`Asset`] is associated doesn't exist. /// - The [`Domain`] with which the [`Account`] is associated doesn't exist. - pub fn asset(&self, id: &::Id) -> Result { + pub fn asset(&self, id: &AssetId) -> Result { self.map_account( &id.account_id, |account| -> Result { @@ -475,7 +486,7 @@ impl WorldStateView { #[allow(clippy::missing_panics_doc)] pub fn asset_or_insert( &mut self, - id: &::Id, + id: &AssetId, default_asset_value: impl Into, ) -> Result { if let Ok(asset) = self.asset(id) { @@ -552,7 +563,7 @@ impl WorldStateView { /// /// # Errors /// Fails if there is no domain - pub fn domain(&self, id: &::Id) -> Result<&Domain, FindError> { + pub fn domain<'wsv>(&'wsv self, id: &DomainId) -> Result<&'wsv Domain, FindError> { let domain = self .world .domains @@ -565,10 +576,7 @@ impl WorldStateView { /// /// # Errors /// Fails if there is no domain - pub fn domain_mut( - &mut self, - id: &::Id, - ) -> Result<&mut Domain, FindError> { + pub fn domain_mut(&mut self, id: &DomainId) -> Result<&mut Domain, FindError> { let domain = self .world .domains @@ -588,16 +596,13 @@ impl WorldStateView { /// # Errors /// Fails if there is no domain #[allow(clippy::panic_in_result_fn)] - pub fn map_domain( - &self, - id: &::Id, - f: impl FnOnce(&Domain) -> Result, + pub fn map_domain<'wsv, T>( + &'wsv self, + id: &DomainId, + f: impl FnOnce(&'wsv Domain) -> T, ) -> Result { let domain = self.domain(id)?; - let value = f(domain).map_or_else( - |_infallible| unreachable!("Returning `Infallible` should not be possible"), - |value| value, - ); + let value = f(domain); Ok(value) } @@ -681,10 +686,10 @@ impl WorldStateView { /// /// # Errors /// Fails if there is no domain or account - pub fn map_account( - &self, + pub fn map_account<'wsv, T>( + &'wsv self, id: &AccountId, - f: impl FnOnce(&Account) -> T, + f: impl FnOnce(&'wsv Account) -> T, ) -> Result { let domain = self.domain(&id.domain_id)?; let account = domain @@ -694,6 +699,15 @@ impl WorldStateView { Ok(f(account)) } + fn account(&self, id: &AccountId) -> Result<&Account, FindError> { + self.domain(&id.domain_id).and_then(|domain| { + domain + .accounts + .get(id) + .ok_or_else(|| FindError::Account(id.clone())) + }) + } + /// Get mutable reference to [`Account`] /// /// # Errors @@ -737,24 +751,13 @@ impl WorldStateView { } /// Get all `PeerId`s without an ability to modify them. - pub fn peers(&self) -> Vec { - let mut vec = self - .world - .trusted_peers_ids - .iter() - .map(|peer| Peer::new((*peer).clone())) - .collect::>(); - vec.sort(); - vec + pub fn peers(&self) -> impl ExactSizeIterator { + self.world.trusted_peers_ids.iter() } /// Get all `Parameter`s registered in the world. - pub fn parameters(&self) -> Vec { - self.world - .parameters - .iter() - .cloned() - .collect::>() + pub fn parameters(&self) -> impl ExactSizeIterator { + self.world.parameters.iter() } /// Query parameter and convert it to a proper type @@ -780,7 +783,7 @@ impl WorldStateView { /// - Asset definition entry not found pub fn asset_definition( &self, - asset_id: &::Id, + asset_id: &AssetDefinitionId, ) -> Result { self.domain(&asset_id.domain_id)? .asset_definitions @@ -795,7 +798,7 @@ impl WorldStateView { /// - Asset definition not found pub fn asset_total_amount( &self, - definition_id: &::Id, + definition_id: &AssetDefinitionId, ) -> Result { self.domain(&definition_id.domain_id)? .asset_total_quantities @@ -811,7 +814,7 @@ impl WorldStateView { /// - Overflow pub fn increase_asset_total_amount( &mut self, - definition_id: &::Id, + definition_id: &AssetDefinitionId, increment: I, ) -> Result<(), Error> where @@ -850,7 +853,7 @@ impl WorldStateView { /// - Not enough quantity pub fn decrease_asset_total_amount( &mut self, - definition_id: &::Id, + definition_id: &AssetDefinitionId, decrement: I, ) -> Result<(), Error> where @@ -882,116 +885,13 @@ impl WorldStateView { Ok(()) } - /// Get all transactions - pub fn transaction_values(&self) -> Vec { - let mut txs = self - .all_blocks() - .flat_map(|block| { - let block = block.as_v1(); - block - .rejected_transactions - .iter() - .cloned() - .map(|versioned_rejected_tx| TransactionQueryResult { - transaction: TransactionValue::RejectedTransaction(versioned_rejected_tx), - block_hash: block.hash(), - }) - .chain( - block - .transactions - .iter() - .cloned() - .map(VersionedSignedTransaction::from) - .map(|versioned_tx| TransactionQueryResult { - transaction: TransactionValue::Transaction(versioned_tx), - block_hash: block.hash(), - }), - ) - .collect::>() - }) - .collect::>(); - txs.sort(); - txs - } - /// Find a [`VersionedSignedTransaction`] by hash. - pub fn transaction_value_by_hash( + pub fn block_with_tx( &self, hash: &HashOf, - ) -> Option { + ) -> Option> { let height = *self.transactions.get(hash)?; - let block = self.kura.get_block_by_height(height)?; - let block_hash = block.as_v1().hash(); - block - .as_v1() - .rejected_transactions - .iter() - .find( - |RejectedTransaction { - transaction, - error: _, - }| transaction.hash() == *hash, - ) - .cloned() - .map(TransactionValue::RejectedTransaction) - .or_else(|| { - block - .as_v1() - .transactions - .iter() - .find(|e| e.hash() == *hash) - .cloned() - .map(VersionedSignedTransaction::from) - .map(TransactionValue::Transaction) - }) - .map(|tx| TransactionQueryResult { - transaction: tx, - block_hash, - }) - } - - /// Get committed and rejected transaction of the account. - pub fn transactions_values_by_account_id( - &self, - account_id: &AccountId, - ) -> Vec { - let mut transactions = self - .all_blocks() - .flat_map(|block_entry| { - let block = block_entry.as_v1(); - let block_hash = block.hash(); - - block - .rejected_transactions - .iter() - .filter( - |RejectedTransaction { - transaction, - error: _, - }| { - transaction.payload().authority == *account_id - }, - ) - .cloned() - .map(TransactionValue::RejectedTransaction) - .chain( - block - .transactions - .iter() - .filter(|tx| &tx.payload().authority == account_id) - .cloned() - .map(VersionedSignedTransaction::from) - .map(TransactionValue::Transaction), - ) - .map(|tx| TransactionQueryResult { - transaction: tx, - block_hash, - }) - .collect::>() - }) - .collect::>(); - transactions.sort(); - transactions + self.kura.get_block_by_height(height) } /// Get an immutable view of the `World`. diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index 97e9b12f4ef..d62cfc0a81b 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -20,7 +20,7 @@ use iroha_config::{ sumeragi::Configuration as SumeragiConfiguration, torii::Configuration as ToriiConfiguration, }; -use iroha_core::prelude::*; +use iroha_core::{prelude::*, smartcontracts::query::Lazy}; use iroha_data_model::{peer::Peer as DataModelPeer, prelude::*}; use iroha_genesis::{GenesisNetwork, RawGenesisBlock}; use iroha_logger::{Configuration as LoggerConfiguration, InstrumentFutures}; @@ -717,7 +717,7 @@ pub trait TestClient: Sized { where R: ValidQuery + Into + Debug + Clone, >::Error: Into, - R::Output: Clone + Debug; + R::Output: Lazy + Clone + Debug; /// Submits instructions with polling /// @@ -732,7 +732,7 @@ pub trait TestClient: Sized { where R: ValidQuery + Into + Debug + Clone, >::Error: Into, - R::Output: Clone + Debug; + R::Output: Lazy + Clone + Debug; /// Polls request till predicate `f` is satisfied, with default period and max attempts. /// @@ -746,7 +746,7 @@ pub trait TestClient: Sized { where R: ValidQuery + Into + Debug + Clone, >::Error: Into, - R::Output: Clone + Debug; + R::Output: Lazy + Clone + Debug; /// Polls request till predicate `f` is satisfied with `period` and `max_attempts` supplied. /// @@ -762,7 +762,7 @@ pub trait TestClient: Sized { where R: ValidQuery + Into + Debug + Clone, >::Error: Into, - R::Output: Clone + Debug; + R::Output: Lazy + Clone + Debug; } impl TestRuntime for Runtime { @@ -857,7 +857,7 @@ impl TestClient for Client { where R: ValidQuery + Into + Debug + Clone, >::Error: Into, - R::Output: Clone + Debug, + R::Output: Lazy + Clone + Debug, { self.submit(instruction) .expect("Failed to submit instruction."); @@ -873,7 +873,7 @@ impl TestClient for Client { where R: ValidQuery + Into + Debug + Clone, >::Error: Into, - R::Output: Clone + Debug, + R::Output: Lazy + Clone + Debug, { self.submit_all(instructions) .expect("Failed to submit instruction."); @@ -890,7 +890,7 @@ impl TestClient for Client { where R: ValidQuery + Into + Debug + Clone, >::Error: Into, - R::Output: Clone + Debug, + R::Output: Lazy + Clone + Debug, { let mut query_result = None; for _ in 0..max_attempts { @@ -911,7 +911,7 @@ impl TestClient for Client { where R: ValidQuery + Into + Debug + Clone, >::Error: Into, - R::Output: Clone + Debug, + R::Output: Lazy + Clone + Debug, { self.poll_request_with_period(request, Configuration::pipeline_time() / 2, 10, f) } diff --git a/data_model/src/metadata.rs b/data_model/src/metadata.rs index ebc8d1438cc..42ac8e61fa6 100644 --- a/data_model/src/metadata.rs +++ b/data_model/src/metadata.rs @@ -32,10 +32,10 @@ pub mod model { Eq, PartialOrd, Ord, - Deserialize, - Serialize, Decode, Encode, + Deserialize, + Serialize, IntoSchema, )] #[display(fmt = "{max_len},{max_entry_byte_size}_ML")] @@ -82,10 +82,10 @@ pub mod model { Eq, PartialOrd, Ord, - Deserialize, - Serialize, Decode, Encode, + Deserialize, + Serialize, IntoSchema, )] #[cfg_attr(feature = "std", derive(thiserror::Error))] @@ -117,10 +117,10 @@ pub enum MetadataError { Eq, PartialOrd, Ord, - Deserialize, - Serialize, Decode, Encode, + Deserialize, + Serialize, IntoSchema, )] #[display(fmt = "Limits are {limits}, while the actual value is {actual}")] diff --git a/data_model/src/predicate.rs b/data_model/src/predicate.rs index 4c905f9e9dc..49f89440b7a 100644 --- a/data_model/src/predicate.rs +++ b/data_model/src/predicate.rs @@ -211,25 +211,6 @@ where /// Predicate combinator for predicates operating on `Value` pub type PredicateBox = GenericPredicateBox; -impl PredicateBox { - #[must_use] - #[inline] - /// Filter [`Value`] using `self`. - pub fn filter(&self, value: Value) -> Value { - match value { - Value::Vec(v) => Value::Vec(v.into_iter().filter(|val| self.applies(val)).collect()), - other => other, - // We're not handling the LimitedMetadata case, because - // the predicate when applied to it is ambiguous. We could - // pattern match on that case, but we should assume that - // metadata (since it's limited) isn't going to be too - // difficult to filter client-side. I actually think that - // Metadata should be restricted in what types it can - // contain. - } - } -} - impl Default for PredicateBox { fn default() -> Self { PredicateBox::Raw(value::ValuePredicate::Pass) diff --git a/data_model/src/query.rs b/data_model/src/query.rs index bf24b04d7b6..d19773937e6 100644 --- a/data_model/src/query.rs +++ b/data_model/src/query.rs @@ -21,7 +21,7 @@ use self::{ }; use crate::{ account::Account, - block::CommittedBlock, + block::VersionedCommittedBlock, seal, transaction::{RejectedTransaction, TransactionPayload, VersionedSignedTransaction}, Identifiable, Value, @@ -173,7 +173,7 @@ pub mod model { RejectedTransaction(RejectedTransaction), } - /// `TransactionQueryResult` is used in `FindAllTransactions` query + /// Output of [`FindAllTransactions`] query #[derive( Debug, Clone, PartialEq, Eq, Getters, Decode, Encode, Deserialize, Serialize, IntoSchema, )] @@ -183,7 +183,22 @@ pub mod model { /// Transaction pub transaction: TransactionValue, /// The hash of the block to which `tx` belongs to - pub block_hash: HashOf, + pub block_hash: HashOf, + } +} + +/// Type returned from [`Metadata`] queries +pub struct MetadataValue(Value); + +impl From for Value { + fn from(source: MetadataValue) -> Self { + source.0 + } +} + +impl From for MetadataValue { + fn from(source: Value) -> Self { + Self(source) } } @@ -423,6 +438,7 @@ pub mod account { use derive_more::Display; + use super::MetadataValue; use crate::prelude::*; queries! { @@ -503,7 +519,7 @@ pub mod account { } impl Query for FindAccountKeyValueByIdAndKey { - type Output = Value; + type Output = MetadataValue; } impl Query for FindAccountsByName { @@ -584,6 +600,7 @@ pub mod asset { use iroha_data_model_derive::model; pub use self::model::*; + use super::MetadataValue; use crate::prelude::*; queries! { @@ -788,11 +805,11 @@ pub mod asset { } impl Query for FindAssetKeyValueByIdAndKey { - type Output = Value; + type Output = MetadataValue; } impl Query for FindAssetDefinitionKeyValueByIdAndKey { - type Output = Value; + type Output = MetadataValue; } impl Query for IsAssetDefinitionOwner { @@ -932,6 +949,7 @@ pub mod domain { use derive_more::Display; + use super::MetadataValue; use crate::prelude::*; queries! { @@ -975,7 +993,7 @@ pub mod domain { } impl Query for FindDomainKeyValueByIdAndKey { - type Output = Value; + type Output = MetadataValue; } impl FindDomainById { @@ -1052,7 +1070,7 @@ pub mod trigger { use derive_more::Display; - use super::Query; + use super::{MetadataValue, Query}; use crate::{ domain::prelude::*, events::FilterBox, @@ -1116,7 +1134,7 @@ pub mod trigger { } impl Query for FindTriggerKeyValueByIdAndKey { - type Output = Value; + type Output = MetadataValue; } impl Query for FindTriggersByDomainId { @@ -1232,7 +1250,7 @@ pub mod transaction { } impl FindTransactionByHash { - ///Construct [`FindTransactionByHash`]. + /// Construct [`FindTransactionByHash`]. pub fn new(hash: impl Into>>) -> Self { Self { hash: hash.into() } } @@ -1327,7 +1345,6 @@ pub mod http { declare_versioned_with_scale!(VersionedSignedQuery 1..2, Debug, Clone, iroha_macro::FromVariant, IntoSchema); declare_versioned_with_scale!(VersionedQueryResult 1..2, Debug, Clone, iroha_macro::FromVariant, IntoSchema); - declare_versioned_with_scale!(VersionedPaginatedQueryResult 1..2, Debug, Clone, iroha_macro::FromVariant, IntoSchema); #[model] pub mod model { @@ -1365,18 +1382,6 @@ pub mod http { /// Signature of the client who sends this query. pub signature: SignatureOf, } - - /// Sized container for all possible Query results. - #[derive( - Debug, Clone, PartialEq, Eq, Decode, Encode, Deserialize, Serialize, IntoSchema, - )] - #[version_with_scale(n = 1, versioned = "VersionedQueryResult")] - #[serde(transparent)] - #[repr(transparent)] - // TODO: This should be a separate type, not just wrap Value because it infects Value - // with variants that can only ever be returned, i.e. can't be used in instructions - // enum QueryResult { ... } - pub struct QueryResult(pub Value); } mod candidate { @@ -1451,16 +1456,14 @@ pub mod http { /// Paginated Query Result // TODO: This is the only structure whose inner fields are exposed. Wrap it in model macro? #[derive(Debug, Clone, Decode, Encode, Deserialize, Serialize, IntoSchema)] - #[version_with_scale(n = 1, versioned = "VersionedPaginatedQueryResult")] - pub struct PaginatedQueryResult { + #[version_with_scale(n = 1, versioned = "VersionedQueryResult")] + pub struct QueryResult { /// The result of the query execution. - pub result: QueryResult, + pub result: Value, /// pagination pub pagination: Pagination, /// sorting pub sorting: Sorting, - /// Total query amount (if applicable) else 0. - pub total: u64, } impl QueryBuilder { @@ -1501,18 +1504,11 @@ pub mod http { } } - impl From for Value { - fn from(source: QueryResult) -> Self { - source.0 - } - } - pub mod prelude { //! The prelude re-exports most commonly used traits, structs and macros from this crate. pub use super::{ - PaginatedQueryResult, QueryBuilder, QueryResult, SignedQuery, - VersionedPaginatedQueryResult, VersionedQueryResult, VersionedSignedQuery, + QueryBuilder, QueryResult, SignedQuery, VersionedQueryResult, VersionedSignedQuery, }; } } diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index 8a3b9cc1927..f2fec592436 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -2120,7 +2120,6 @@ ] }, "Hash": "Array", - "HashOf": "Hash", "HashOf>": "Hash", "HashOf": "Hash", "HashOf": "Hash", @@ -3043,30 +3042,6 @@ "OriginFilter": "PeerId", "OriginFilter": "RoleId", "OriginFilter": "TriggerId", - "PaginatedQueryResult": { - "Struct": [ - { - "name": "result", - "type": "QueryResult" - }, - { - "name": "filter", - "type": "GenericPredicateBox" - }, - { - "name": "pagination", - "type": "Pagination" - }, - { - "name": "sorting", - "type": "Sorting" - }, - { - "name": "total", - "type": "u64" - } - ] - }, "Pagination": { "Struct": [ { @@ -3604,7 +3579,22 @@ } ] }, - "QueryResult": "Value", + "QueryResult": { + "Struct": [ + { + "name": "result", + "type": "Value" + }, + { + "name": "pagination", + "type": "Pagination" + }, + { + "name": "sorting", + "type": "Sorting" + } + ] + }, "RaiseTo": { "Struct": [ { @@ -4214,7 +4204,7 @@ }, { "name": "block_hash", - "type": "HashOf" + "type": "HashOf" } ] }, @@ -4828,12 +4818,12 @@ } ] }, - "VersionedPaginatedQueryResult": { + "VersionedQueryResult": { "Enum": [ { "tag": "V1", "discriminant": 1, - "type": "PaginatedQueryResult" + "type": "QueryResult" } ] }, diff --git a/schema/gen/src/lib.rs b/schema/gen/src/lib.rs index 9026a56e91e..5bf8d03944c 100644 --- a/schema/gen/src/lib.rs +++ b/schema/gen/src/lib.rs @@ -47,7 +47,7 @@ pub fn build_schemas() -> MetaMap { VersionedBlockSubscriptionRequest, VersionedEventMessage, VersionedEventSubscriptionRequest, - VersionedPaginatedQueryResult, + VersionedQueryResult, VersionedSignedQuery, // Never referenced, but present in type signature. Like `PhantomData` @@ -282,7 +282,7 @@ types!( OriginFilter, OriginFilter, OriginFilter, - PaginatedQueryResult, + QueryResult, Pagination, Pair, Parameter, @@ -308,7 +308,6 @@ types!( QueryBox, QueryExecutionFail, QueryPayload, - QueryResult, RaiseTo, RegisterBox, RegistrableBox, @@ -380,7 +379,7 @@ types!( VersionedCommittedBlockWrapper, VersionedEventMessage, VersionedEventSubscriptionRequest, - VersionedPaginatedQueryResult, + VersionedQueryResult, VersionedSignedQuery, VersionedSignedTransaction, WasmExecutionFail,