From 2dcde0eb99ebf1721ff2873afe9b97fecb50cc54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Ver=C5=A1i=C4=87?= Date: Wed, 28 Feb 2024 15:50:38 +0300 Subject: [PATCH] [refactor] #4315: split pipeline events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marin Veršić --- CONTRIBUTING.md | 10 +- cli/Cargo.toml | 12 +- cli/README.md | 4 +- cli/src/lib.rs | 12 +- client/Cargo.toml | 12 +- client/benches/tps/utils.rs | 7 +- client/src/client.rs | 98 ++-- client/src/config.rs | 2 +- client/src/http.rs | 6 +- client/tests/integration/asset.rs | 13 +- .../integration/domain_owner_permissions.rs | 9 +- client/tests/integration/events/data.rs | 4 +- .../tests/integration/events/notification.rs | 16 +- client/tests/integration/events/pipeline.rs | 63 +-- client/tests/integration/permissions.rs | 20 +- client/tests/integration/roles.rs | 9 +- .../src/lib.rs | 2 +- .../mint_rose_trigger/src/lib.rs | 2 +- .../query_assets_and_save_cursor/src/lib.rs | 2 +- .../integration/triggers/by_call_trigger.rs | 8 +- .../integration/triggers/time_trigger.rs | 38 +- client_cli/src/main.rs | 13 +- config/Cargo.toml | 2 +- config/src/parameters/defaults.rs | 12 +- config/src/parameters/user.rs | 10 +- config/src/parameters/user/boilerplate.rs | 8 +- config/tests/fixtures/full.toml | 2 +- configs/peer.template.toml | 2 +- configs/swarm/executor.wasm | Bin 533713 -> 535102 bytes core/Cargo.toml | 8 +- core/benches/blocks/apply_blocks.rs | 8 +- core/benches/blocks/common.rs | 2 + core/benches/blocks/validate_blocks.rs | 15 +- .../blocks/validate_blocks_benchmark.rs | 6 +- .../benches/blocks/validate_blocks_oneshot.rs | 4 +- core/benches/kura.rs | 1 + core/benches/validation.rs | 2 +- core/src/block.rs | 330 +++++++++---- core/src/block_sync.rs | 27 +- core/src/kura.rs | 6 +- core/src/lib.rs | 6 +- core/src/queue.rs | 126 +++-- core/src/smartcontracts/isi/query.rs | 6 + core/src/smartcontracts/isi/triggers/set.rs | 32 +- .../isi/triggers/specialized.rs | 4 +- core/src/smartcontracts/wasm.rs | 4 +- core/src/state.rs | 84 ++-- core/src/sumeragi/main_loop.rs | 262 +++++------ core/src/sumeragi/message.rs | 16 +- core/src/sumeragi/mod.rs | 43 +- core/src/tx.rs | 2 +- core/test_network/Cargo.toml | 2 +- core/test_network/src/lib.rs | 14 +- crypto/src/lib.rs | 7 - data_model/derive/src/enum_ref.rs | 2 +- data_model/derive/src/lib.rs | 52 ++- data_model/derive/src/model.rs | 11 +- data_model/src/account.rs | 2 + data_model/src/block.rs | 84 ++-- data_model/src/events/data/filters.rs | 1 - data_model/src/events/mod.rs | 124 +++-- data_model/src/events/pipeline.rs | 441 +++++++++++------- data_model/src/lib.rs | 13 - data_model/src/query/mod.rs | 2 +- data_model/src/query/predicate.rs | 2 +- data_model/src/smart_contract.rs | 2 +- data_model/src/transaction.rs | 38 +- data_model/src/trigger.rs | 16 +- docs/source/references/schema.json | 241 ++++++---- logger/Cargo.toml | 6 +- logger/src/lib.rs | 4 +- schema/gen/src/lib.rs | 35 +- smart_contract/executor/src/default.rs | 2 +- smart_contract/executor/src/permission.rs | 6 +- smart_contract/src/lib.rs | 4 +- telemetry/Cargo.toml | 4 +- telemetry/derive/src/lib.rs | 23 +- telemetry/src/lib.rs | 2 +- telemetry/src/metrics.rs | 16 +- tools/parity_scale_decoder/Cargo.toml | 2 +- tools/parity_scale_decoder/README.md | 6 +- tools/parity_scale_decoder/src/main.rs | 5 +- torii/src/event.rs | 10 +- 83 files changed, 1497 insertions(+), 1074 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6bc087b1ba..51cb1f0cc10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -242,12 +242,12 @@ Set the `LOG_FILE_PATH` environment variable to an appropriate location to store
Expand to learn how to compile iroha with tokio console support. -Sometimes it might be helpful for debugging to analyze tokio tasks using [tokio-console](https://github.com/tokio-rs/console). +Sometimes it might be helpful for debugging to analyze tokio tasks using [tokio_console](https://github.com/tokio-rs/console). In this case you should compile iroha with support of tokio console like that: ```bash -RUSTFLAGS="--cfg tokio_unstable" cargo build --features tokio-console +RUSTFLAGS="--cfg tokio_unstable" cargo build --features tokio_console ``` Port for tokio console can by configured through `LOG_TOKIO_CONSOLE_ADDR` configuration parameter (or environment variable). @@ -257,11 +257,11 @@ Example of running iroha with tokio console support using `scripts/test_env.sh`: ```bash # 1. Compile iroha -RUSTFLAGS="--cfg tokio_unstable" cargo build --features tokio-console +RUSTFLAGS="--cfg tokio_unstable" cargo build --features tokio_console # 2. Run iroha with TRACE log level LOG_LEVEL=TRACE ./scripts/test_env.sh setup # 3. Access iroha. Peers will be available on ports 5555, 5556, ... -tokio-console http://127.0.0.1:5555 +tokio_console http://127.0.0.1:5555 ```
@@ -272,7 +272,7 @@ tokio-console http://127.0.0.1:5555 To optimize performance it's useful to profile iroha. -To do that you should compile iroha with `profiling` profile and with `profiling` feature: +To do that you should compile iroha with `profiling` profile and with `profiling` feature: ```bash RUSTFLAGS="-C force-frame-pointers=on" cargo +nightly -Z build-std build --target your-desired-target --profile profiling --features profiling diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4ab631e22e2..da3f1a714e5 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -18,19 +18,19 @@ categories.workspace = true workspace = true [features] -default = ["telemetry", "schema-endpoint"] +default = ["telemetry", "schema_endpoint"] # Support lightweight telemetry, including diagnostics telemetry = ["iroha_telemetry", "iroha_core/telemetry", "iroha_torii/telemetry"] # Support developer-specific telemetry. # Should not be enabled on production builds. -dev-telemetry = ["iroha_core/dev-telemetry", "iroha_telemetry"] +dev_telemetry = ["iroha_core/dev_telemetry", "iroha_telemetry"] # Support schema generation from the `schema` endpoint in the local binary. # Useful for debugging issues with decoding in SDKs. -schema-endpoint = ["iroha_torii/schema"] +schema_endpoint = ["iroha_torii/schema"] # Support internal testing infrastructure for integration tests. # Disable in production. -test-network = ["thread-local-panic-hook"] +test_network = ["thread-local-panic-hook"] [badges] is-it-maintained-issue-resolution = { repository = "https://github.com/hyperledger/iroha" } @@ -79,8 +79,8 @@ vergen = { workspace = true, features = ["cargo"] } [package.metadata.cargo-all-features] denylist = [ - "schema-endpoint", + "schema_endpoint", "telemetry", - "test-network", + "test_network", ] skip_optional_dependencies = true diff --git a/cli/README.md b/cli/README.md index 5ba8d269b39..6ecceb3f6f9 100644 --- a/cli/README.md +++ b/cli/README.md @@ -25,14 +25,14 @@ The results of the compilation can be found in `/target/release To add optional features, use ``--features``. For example, to add the support for _dev_telemetry_, run: ```bash -cargo build --release --features dev-telemetry +cargo build --release --features dev_telemetry ``` A full list of features can be found in the [cargo manifest file](Cargo.toml) for this crate. ### Disable default features -By default, the Iroha binary is compiled with the `telemetry`, and `schema-endpoint` features. If you wish to remove those features, add `--no-default-features` to the command. +By default, the Iroha binary is compiled with the `telemetry`, and `schema_endpoint` features. If you wish to remove those features, add `--no-default-features` to the command. ```bash cargo build --release --no-default-features diff --git a/cli/src/lib.rs b/cli/src/lib.rs index eb631467a21..c6c08a7747f 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -140,7 +140,7 @@ impl NetworkRelay { impl Iroha { fn prepare_panic_hook(notify_shutdown: Arc) { - #[cfg(not(feature = "test-network"))] + #[cfg(not(feature = "test_network"))] use std::panic::set_hook; // This is a hot-fix for tests @@ -160,7 +160,7 @@ impl Iroha { // // Remove this when all Rust integrations tests will be converted to a // separate Python tests. - #[cfg(feature = "test-network")] + #[cfg(feature = "test_network")] use thread_local_panic_hook::set_hook; set_hook(Box::new(move |info| { @@ -251,7 +251,7 @@ impl Iroha { }); let state = Arc::new(state); - let queue = Arc::new(Queue::from_config(config.queue)); + let queue = Arc::new(Queue::from_config(config.queue, events_sender.clone())); match Self::start_telemetry(&logger, &config).await? { TelemetryStartStatus::Started => iroha_logger::info!("Telemetry started"), TelemetryStartStatus::NotStarted => iroha_logger::warn!("Telemetry not started"), @@ -369,7 +369,7 @@ impl Iroha { /// /// # Errors /// - Forwards initialisation error. - #[cfg(feature = "test-network")] + #[cfg(feature = "test_network")] pub fn start_as_task(&mut self) -> Result>> { iroha_logger::info!("Starting Iroha as task"); let torii = self @@ -386,7 +386,7 @@ impl Iroha { logger: &LoggerHandle, config: &Config, ) -> Result { - #[cfg(feature = "dev-telemetry")] + #[cfg(feature = "dev_telemetry")] { if let Some(config) = &config.dev_telemetry { let receiver = logger @@ -539,7 +539,7 @@ mod tests { use super::*; - #[cfg(not(feature = "test-network"))] + #[cfg(not(feature = "test_network"))] mod no_test_network { use std::{iter::repeat, panic, thread}; diff --git a/client/Cargo.toml b/client/Cargo.toml index 1d38df505b9..4ae8ad2a1f1 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -24,24 +24,24 @@ maintenance = { status = "actively-developed" } [features] # Use rustls by default to avoid OpenSSL dependency, simplifying compilation with musl -default = ["tls-rustls-native-roots"] +default = ["tls_rustls_native_roots"] -tls-native = [ +tls_native = [ "attohttpc/tls-native", "tokio-tungstenite/native-tls", "tungstenite/native-tls", ] -tls-native-vendored = [ +tls_native_vendored = [ "attohttpc/tls-native-vendored", "tokio-tungstenite/native-tls-vendored", "tungstenite/native-tls-vendored", ] -tls-rustls-native-roots = [ +tls_rustls_native_roots = [ "attohttpc/tls-rustls-native-roots", "tokio-tungstenite/rustls-tls-native-roots", "tungstenite/rustls-tls-native-roots", ] -tls-rustls-webpki-roots = [ +tls_rustls_webpki_roots = [ "attohttpc/tls-rustls-webpki-roots", "tokio-tungstenite/rustls-tls-webpki-roots", "tungstenite/rustls-tls-webpki-roots", @@ -83,7 +83,7 @@ iroha_wasm_builder = { workspace = true } # TODO: These three activate `transparent_api` but client should never activate this feature. # Additionally there is a dependency on iroha_core in dev-dependencies in telemetry/derive # Hopefully, once the integration tests migration is finished these can be removed -iroha = { workspace = true, features = ["dev-telemetry", "telemetry"] } +iroha = { workspace = true, features = ["dev_telemetry", "telemetry"] } iroha_genesis = { workspace = true } test_network = { workspace = true } diff --git a/client/benches/tps/utils.rs b/client/benches/tps/utils.rs index d215d1ce203..6e2f74d83fc 100644 --- a/client/benches/tps/utils.rs +++ b/client/benches/tps/utils.rs @@ -18,6 +18,7 @@ use iroha_client::{ prelude::*, }, }; +use iroha_data_model::events::pipeline::{BlockEventFilter, BlockStatus}; use serde::Deserialize; use test_network::*; @@ -172,13 +173,11 @@ impl MeasurerUnit { fn spawn_event_counter(&self) -> thread::JoinHandle> { let listener = self.client.clone(); let (init_sender, init_receiver) = mpsc::channel(); - let event_filter = PipelineEventFilter::new() - .for_entity(PipelineEntityKind::Block) - .for_status(PipelineStatusKind::Committed); + let event_filter = BlockEventFilter::default().for_status(BlockStatus::Applied); let blocks_expected = self.config.blocks as usize; let name = self.name; let handle = thread::spawn(move || -> Result<()> { - let mut event_iterator = listener.listen_for_events(event_filter)?; + let mut event_iterator = listener.listen_for_events([event_filter])?; init_sender.send(())?; for i in 1..=blocks_expected { let _event = event_iterator.next().expect("Event stream closed")?; diff --git a/client/src/client.rs b/client/src/client.rs index ce942c1752c..b30a0d67193 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -14,7 +14,13 @@ use eyre::{eyre, Result, WrapErr}; use futures_util::StreamExt; use http_default::{AsyncWebSocketStream, WebSocketStream}; pub use iroha_config::client_api::ConfigDTO; -use iroha_data_model::query::QueryOutputBox; +use iroha_data_model::{ + events::pipeline::{ + BlockEventFilter, BlockStatus, PipelineEventBox, PipelineEventFilterBox, + TransactionEventFilter, TransactionStatus, + }, + query::QueryOutputBox, +}; use iroha_logger::prelude::*; use iroha_telemetry::metrics::Status; use iroha_torii_const::uri as torii_uri; @@ -603,14 +609,19 @@ impl Client { rt.block_on(async { let mut event_iterator = { - let event_iterator_result = tokio::time::timeout_at( - deadline, - self.listen_for_events_async(PipelineEventFilter::new().for_hash(hash.into())), - ) - .await - .map_err(Into::into) - .and_then(std::convert::identity) - .wrap_err("Failed to establish event listener connection"); + let filters = vec![ + TransactionEventFilter::default().for_hash(hash).into(), + PipelineEventFilterBox::from( + BlockEventFilter::default().for_status(BlockStatus::Applied), + ), + ]; + + let event_iterator_result = + tokio::time::timeout_at(deadline, self.listen_for_events_async(filters)) + .await + .map_err(Into::into) + .and_then(std::convert::identity) + .wrap_err("Failed to establish event listener connection"); let _send_result = init_sender.send(event_iterator_result.is_ok()); event_iterator_result? }; @@ -631,17 +642,34 @@ impl Client { event_iterator: &mut AsyncEventStream, hash: HashOf, ) -> Result> { + let mut block_height = None; + while let Some(event) = event_iterator.next().await { - if let Event::Pipeline(this_event) = event? { - match this_event.status() { - PipelineStatus::Validating => {} - PipelineStatus::Rejected(ref reason) => { - return Err(reason.clone().into()); + if let EventBox::Pipeline(this_event) = event? { + match this_event { + PipelineEventBox::Transaction(transaction_event) => { + match transaction_event.status() { + TransactionStatus::Queued => {} + TransactionStatus::Approved => { + block_height = transaction_event.block_height; + } + TransactionStatus::Rejected(reason) => { + return Err((Clone::clone(&**reason)).into()); + } + TransactionStatus::Expired => return Err(eyre!("Transaction expired")), + } + } + PipelineEventBox::Block(block_event) => { + if Some(block_event.header().height()) == block_height { + if let BlockStatus::Applied = block_event.status() { + return Ok(hash); + } + } } - PipelineStatus::Committed => return Ok(hash), } } } + Err(eyre!( "Connection dropped without `Committed` or `Rejected` event" )) @@ -903,11 +931,9 @@ impl Client { /// - Forwards from [`events_api::EventIterator::new`] pub fn listen_for_events( &self, - event_filter: impl Into, - ) -> Result>> { - let event_filter = event_filter.into(); - iroha_logger::trace!(?event_filter); - events_api::EventIterator::new(self.events_handler(event_filter)?) + event_filters: impl IntoIterator>, + ) -> Result>> { + events_api::EventIterator::new(self.events_handler(event_filters)?) } /// Connect asynchronously (through `WebSocket`) to listen for `Iroha` `pipeline` and `data` events. @@ -917,11 +943,9 @@ impl Client { /// - Forwards from [`events_api::AsyncEventStream::new`] pub async fn listen_for_events_async( &self, - event_filter: impl Into + Send, + event_filters: impl IntoIterator> + Send, ) -> Result { - let event_filter = event_filter.into(); - iroha_logger::trace!(?event_filter, "Async listening with"); - events_api::AsyncEventStream::new(self.events_handler(event_filter)?).await + events_api::AsyncEventStream::new(self.events_handler(event_filters)?).await } /// Constructs an Events API handler. With it, you can use any WS client you want. @@ -931,10 +955,10 @@ impl Client { #[inline] pub fn events_handler( &self, - event_filter: impl Into, + event_filters: impl IntoIterator>, ) -> Result { events_api::flow::Init::new( - event_filter.into(), + event_filters, self.headers.clone(), self.torii_url .join(torii_uri::SUBSCRIPTION) @@ -1237,12 +1261,12 @@ pub mod events_api { /// Initialization struct for Events API flow. pub struct Init { - /// Event filter - filter: EventFilterBox, - /// HTTP request headers - headers: HashMap, /// TORII URL url: Url, + /// HTTP request headers + headers: HashMap, + /// Event filter + filters: Vec, } impl Init { @@ -1252,14 +1276,14 @@ pub mod events_api { /// Fails if [`transform_ws_url`] fails. #[inline] pub(in super::super) fn new( - filter: EventFilterBox, + filters: impl IntoIterator>, headers: HashMap, url: Url, ) -> Result { Ok(Self { - filter, - headers, url: transform_ws_url(url)?, + headers, + filters: filters.into_iter().map(Into::into).collect(), }) } } @@ -1269,12 +1293,12 @@ pub mod events_api { fn init(self) -> InitData { let Self { - filter, - headers, url, + headers, + filters, } = self; - let msg = EventSubscriptionRequest::new(filter).encode(); + let msg = EventSubscriptionRequest::new(filters).encode(); InitData::new(R::new(HttpMethod::GET, url).headers(headers), msg, Events) } } @@ -1284,7 +1308,7 @@ pub mod events_api { pub struct Events; impl FlowEvents for Events { - type Event = crate::data_model::prelude::Event; + type Event = crate::data_model::prelude::EventBox; fn message(&self, message: Vec) -> Result { let event_socket_message = EventMessage::decode_all(&mut message.as_slice())?; diff --git a/client/src/config.rs b/client/src/config.rs index 34b7e8663c7..72bb909d8c7 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -9,7 +9,7 @@ use iroha_config::{ base, base::{FromEnv, StdEnv, UnwrapPartial}, }; -use iroha_crypto::prelude::*; +use iroha_crypto::KeyPair; use iroha_data_model::{prelude::*, ChainId}; use iroha_primitives::small::SmallStr; use serde::{Deserialize, Serialize}; diff --git a/client/src/http.rs b/client/src/http.rs index 40ea3b923b0..905c4965838 100644 --- a/client/src/http.rs +++ b/client/src/http.rs @@ -150,7 +150,7 @@ pub mod ws { /// use eyre::Result; /// use url::Url; /// use iroha_client::{ - /// data_model::prelude::Event, + /// data_model::prelude::EventBox, /// client::events_api::flow as events_api_flow, /// http::{ /// ws::conn_flow::{Events, Init, InitData}, @@ -203,7 +203,7 @@ pub mod ws { /// } /// } /// - /// fn collect_5_events(flow: events_api_flow::Init) -> Result> { + /// fn collect_5_events(flow: events_api_flow::Init) -> Result> { /// // Constructing initial flow data /// let InitData { /// next: flow, @@ -216,7 +216,7 @@ pub mod ws { /// stream.send(first_message); /// /// // And now we are able to collect events - /// let mut events: Vec = Vec::with_capacity(5); + /// let mut events: Vec = Vec::with_capacity(5); /// while events.len() < 5 { /// let msg = stream.get_next(); /// let event = flow.message(msg)?; diff --git a/client/tests/integration/asset.rs b/client/tests/integration/asset.rs index fe95e30f348..34a102afd93 100644 --- a/client/tests/integration/asset.rs +++ b/client/tests/integration/asset.rs @@ -10,6 +10,7 @@ use iroha_config::parameters::actual::Root as Config; use iroha_data_model::{ asset::{AssetId, AssetValue, AssetValueType}, isi::error::{InstructionEvaluationError, InstructionExecutionError, Mismatch, TypeError}, + transaction::error::TransactionRejectionReason, }; use serde_json::json; use test_network::*; @@ -463,17 +464,17 @@ fn fail_if_dont_satisfy_spec() { .expect_err("Should be rejected due to non integer value"); let rejection_reason = err - .downcast_ref::() - .unwrap_or_else(|| panic!("Error {err} is not PipelineRejectionReason")); + .downcast_ref::() + .unwrap_or_else(|| panic!("Error {err} is not TransactionRejectionReason")); assert_eq!( rejection_reason, - &PipelineRejectionReason::Transaction(TransactionRejectionReason::Validation( - ValidationFail::InstructionFailed(InstructionExecutionError::Evaluate( - InstructionEvaluationError::Type(TypeError::from(Mismatch { + &TransactionRejectionReason::Validation(ValidationFail::InstructionFailed( + InstructionExecutionError::Evaluate(InstructionEvaluationError::Type( + TypeError::from(Mismatch { expected: AssetValueType::Numeric(NumericSpec::integer()), actual: AssetValueType::Numeric(NumericSpec::fractional(2)) - })) + }) )) )) ); diff --git a/client/tests/integration/domain_owner_permissions.rs b/client/tests/integration/domain_owner_permissions.rs index e0945b85f70..af78eff12ac 100644 --- a/client/tests/integration/domain_owner_permissions.rs +++ b/client/tests/integration/domain_owner_permissions.rs @@ -3,6 +3,7 @@ use iroha_client::{ crypto::KeyPair, data_model::{account::SignatureCheckCondition, prelude::*}, }; +use iroha_data_model::transaction::error::TransactionRejectionReason; use serde_json::json; use test_network::*; @@ -37,14 +38,12 @@ fn domain_owner_domain_permissions() -> Result<()> { .expect_err("Tx should fail due to permissions"); let rejection_reason = err - .downcast_ref::() - .unwrap_or_else(|| panic!("Error {err} is not PipelineRejectionReason")); + .downcast_ref::() + .unwrap_or_else(|| panic!("Error {err} is not TransactionRejectionReason")); assert!(matches!( rejection_reason, - &PipelineRejectionReason::Transaction(TransactionRejectionReason::Validation( - ValidationFail::NotPermitted(_) - )) + &TransactionRejectionReason::Validation(ValidationFail::NotPermitted(_)) )); // "alice@wonderland" owns the domain and can register AssetDefinitions by default as domain owner diff --git a/client/tests/integration/events/data.rs b/client/tests/integration/events/data.rs index 4250ff2b682..9a6d6986cc2 100644 --- a/client/tests/integration/events/data.rs +++ b/client/tests/integration/events/data.rs @@ -140,7 +140,7 @@ fn transaction_execution_should_produce_events( let (event_sender, event_receiver) = mpsc::channel(); let event_filter = DataEventFilter::Any; thread::spawn(move || -> Result<()> { - let event_iterator = listener.listen_for_events(event_filter)?; + let event_iterator = listener.listen_for_events([event_filter])?; init_sender.send(())?; for event in event_iterator { event_sender.send(event)? @@ -184,7 +184,7 @@ fn produce_multiple_events() -> Result<()> { let (event_sender, event_receiver) = mpsc::channel(); let event_filter = DataEventFilter::Any; thread::spawn(move || -> Result<()> { - let event_iterator = listener.listen_for_events(event_filter)?; + let event_iterator = listener.listen_for_events([event_filter])?; init_sender.send(())?; for event in event_iterator { event_sender.send(event)? diff --git a/client/tests/integration/events/notification.rs b/client/tests/integration/events/notification.rs index bf26feb351b..c060d1e1e64 100644 --- a/client/tests/integration/events/notification.rs +++ b/client/tests/integration/events/notification.rs @@ -33,11 +33,9 @@ fn trigger_completion_success_should_produce_event() -> Result<()> { let thread_client = test_client.clone(); let (sender, receiver) = mpsc::channel(); let _handle = thread::spawn(move || -> Result<()> { - let mut event_it = thread_client.listen_for_events( - TriggerCompletedEventFilter::new() - .for_trigger(trigger_id) - .for_outcome(TriggerCompletedOutcomeType::Success), - )?; + let mut event_it = thread_client.listen_for_events([TriggerCompletedEventFilter::new() + .for_trigger(trigger_id) + .for_outcome(TriggerCompletedOutcomeType::Success)])?; if event_it.next().is_some() { sender.send(())?; return Ok(()); @@ -79,11 +77,9 @@ fn trigger_completion_failure_should_produce_event() -> Result<()> { let thread_client = test_client.clone(); let (sender, receiver) = mpsc::channel(); let _handle = thread::spawn(move || -> Result<()> { - let mut event_it = thread_client.listen_for_events( - TriggerCompletedEventFilter::new() - .for_trigger(trigger_id) - .for_outcome(TriggerCompletedOutcomeType::Failure), - )?; + let mut event_it = thread_client.listen_for_events([TriggerCompletedEventFilter::new() + .for_trigger(trigger_id) + .for_outcome(TriggerCompletedOutcomeType::Failure)])?; if event_it.next().is_some() { sender.send(())?; return Ok(()); diff --git a/client/tests/integration/events/pipeline.rs b/client/tests/integration/events/pipeline.rs index 30f17528219..cd8288e0f05 100644 --- a/client/tests/integration/events/pipeline.rs +++ b/client/tests/integration/events/pipeline.rs @@ -9,6 +9,14 @@ use iroha_client::{ }, }; use iroha_config::parameters::actual::Root as Config; +use iroha_data_model::{ + events::pipeline::{ + BlockEvent, BlockEventFilter, BlockStatus, TransactionEventFilter, TransactionStatus, + }, + isi::error::InstructionExecutionError, + transaction::error::TransactionRejectionReason, + ValidationFail, +}; use test_network::*; // Needed to re-enable ignored tests. @@ -17,24 +25,28 @@ const PEER_COUNT: usize = 7; #[ignore = "ignore, more in #2851"] #[test] fn transaction_with_no_instructions_should_be_committed() -> Result<()> { - test_with_instruction_and_status_and_port(None, PipelineStatusKind::Committed, 10_250) + test_with_instruction_and_status_and_port(None, &TransactionStatus::Approved, 10_250) } #[ignore = "ignore, more in #2851"] // #[ignore = "Experiment"] #[test] fn transaction_with_fail_instruction_should_be_rejected() -> Result<()> { - let fail = Fail::new("Should be rejected".to_owned()); + let msg = "Should be rejected".to_owned(); + + let fail = Fail::new(msg.clone()); test_with_instruction_and_status_and_port( Some(fail.into()), - PipelineStatusKind::Rejected, + &TransactionStatus::Rejected(Box::new(TransactionRejectionReason::Validation( + ValidationFail::InstructionFailed(InstructionExecutionError::Fail(msg)), + ))), 10_350, ) } fn test_with_instruction_and_status_and_port( instruction: Option, - should_be: PipelineStatusKind, + should_be: &TransactionStatus, port: u16, ) -> Result<()> { let (_rt, network, client) = @@ -56,9 +68,9 @@ fn test_with_instruction_and_status_and_port( let mut handles = Vec::new(); for listener in clients { let checker = Checker { listener, hash }; - let handle_validating = checker.clone().spawn(PipelineStatusKind::Validating); + let handle_validating = checker.clone().spawn(TransactionStatus::Queued); handles.push(handle_validating); - let handle_validated = checker.spawn(should_be); + let handle_validated = checker.spawn(should_be.clone()); handles.push(handle_validated); } // When @@ -78,16 +90,13 @@ struct Checker { } impl Checker { - fn spawn(self, status_kind: PipelineStatusKind) -> JoinHandle<()> { + fn spawn(self, status_kind: TransactionStatus) -> JoinHandle<()> { thread::spawn(move || { let mut event_iterator = self .listener - .listen_for_events( - PipelineEventFilter::new() - .for_entity(PipelineEntityKind::Transaction) - .for_status(status_kind) - .for_hash(*self.hash), - ) + .listen_for_events([TransactionEventFilter::default() + .for_status(status_kind) + .for_hash(self.hash)]) .expect("Failed to create event iterator."); let event_result = event_iterator.next().expect("Stream closed"); let _event = event_result.expect("Must be valid"); @@ -96,36 +105,30 @@ impl Checker { } #[test] -fn committed_block_must_be_available_in_kura() { +fn applied_block_must_be_available_in_kura() { let (_rt, peer, client) = ::new().with_port(11_040).start_with_runtime(); wait_for_genesis_committed(&[client.clone()], 0); - let event_filter = PipelineEventFilter::new() - .for_entity(PipelineEntityKind::Block) - .for_status(PipelineStatusKind::Committed); + let event_filter = BlockEventFilter::default().for_status(BlockStatus::Applied); let mut event_iter = client - .listen_for_events(event_filter) + .listen_for_events([event_filter]) .expect("Failed to subscribe for events"); client .submit(Fail::new("Dummy instruction".to_owned())) .expect("Failed to submit transaction"); - let event = event_iter.next().expect("Block must be committed"); - let Ok(Event::Pipeline(PipelineEvent { - entity_kind: PipelineEntityKind::Block, - status: PipelineStatus::Committed, - hash, - })) = event - else { - panic!("Received unexpected event") - }; - let hash = HashOf::from_untyped_unchecked(hash); + let event: BlockEvent = event_iter + .next() + .expect("Block must be committed") + .expect("Block must be committed") + .try_into() + .expect("Received unexpected event"); peer.iroha .as_ref() .expect("Must be some") .kura - .get_block_height_by_hash(&hash) - .expect("Block committed event was received earlier"); + .get_block_by_height(event.header().height()) + .expect("Block applied event was received earlier"); } diff --git a/client/tests/integration/permissions.rs b/client/tests/integration/permissions.rs index e7fea53ac18..9a4578b8660 100644 --- a/client/tests/integration/permissions.rs +++ b/client/tests/integration/permissions.rs @@ -6,7 +6,9 @@ use iroha_client::{ crypto::KeyPair, data_model::prelude::*, }; -use iroha_data_model::permission::PermissionToken; +use iroha_data_model::{ + permission::PermissionToken, transaction::error::TransactionRejectionReason, +}; use iroha_genesis::GenesisNetwork; use serde_json::json; use test_network::{PeerBuilder, *}; @@ -104,14 +106,12 @@ fn permissions_disallow_asset_transfer() { .submit_transaction_blocking(&transfer_tx) .expect_err("Transaction was not rejected."); let rejection_reason = err - .downcast_ref::() - .expect("Error {err} is not PipelineRejectionReason"); + .downcast_ref::() + .expect("Error {err} is not TransactionRejectionReason"); //Then assert!(matches!( rejection_reason, - &PipelineRejectionReason::Transaction(TransactionRejectionReason::Validation( - ValidationFail::NotPermitted(_) - )) + &TransactionRejectionReason::Validation(ValidationFail::NotPermitted(_)) )); let alice_assets = get_assets(&iroha_client, &alice_id); assert_eq!(alice_assets, alice_start_assets); @@ -156,14 +156,12 @@ fn permissions_disallow_asset_burn() { .submit_transaction_blocking(&burn_tx) .expect_err("Transaction was not rejected."); let rejection_reason = err - .downcast_ref::() - .expect("Error {err} is not PipelineRejectionReason"); + .downcast_ref::() + .expect("Error {err} is not TransactionRejectionReason"); assert!(matches!( rejection_reason, - &PipelineRejectionReason::Transaction(TransactionRejectionReason::Validation( - ValidationFail::NotPermitted(_) - )) + &TransactionRejectionReason::Validation(ValidationFail::NotPermitted(_)) )); let alice_assets = get_assets(&iroha_client, &alice_id); diff --git a/client/tests/integration/roles.rs b/client/tests/integration/roles.rs index 12a03f333c1..6f260e3709f 100644 --- a/client/tests/integration/roles.rs +++ b/client/tests/integration/roles.rs @@ -6,6 +6,7 @@ use iroha_client::{ crypto::KeyPair, data_model::prelude::*, }; +use iroha_data_model::transaction::error::TransactionRejectionReason; use serde_json::json; use test_network::*; @@ -164,14 +165,12 @@ fn role_with_invalid_permissions_is_not_accepted() -> Result<()> { .expect_err("Submitting role with invalid permission token should fail"); let rejection_reason = err - .downcast_ref::() - .unwrap_or_else(|| panic!("Error {err} is not PipelineRejectionReason")); + .downcast_ref::() + .unwrap_or_else(|| panic!("Error {err} is not TransactionRejectionReason")); assert!(matches!( rejection_reason, - &PipelineRejectionReason::Transaction(TransactionRejectionReason::Validation( - ValidationFail::NotPermitted(_) - )) + &TransactionRejectionReason::Validation(ValidationFail::NotPermitted(_)) )); Ok(()) 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 2a725479740..8eff37089b2 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 @@ -16,7 +16,7 @@ static ALLOC: LockedAllocator = LockedAllocator::new(FreeList getrandom::register_custom_getrandom!(iroha_trigger::stub_getrandom); #[iroha_trigger::main] -fn main(_owner: AccountId, _event: Event) { +fn main(_owner: AccountId, _event: EventBox) { iroha_trigger::log::info!("Executing trigger"); let accounts_cursor = FindAllAccounts.execute().dbg_unwrap(); diff --git a/client/tests/integration/smartcontracts/mint_rose_trigger/src/lib.rs b/client/tests/integration/smartcontracts/mint_rose_trigger/src/lib.rs index e3558de7c61..701956c8ad8 100644 --- a/client/tests/integration/smartcontracts/mint_rose_trigger/src/lib.rs +++ b/client/tests/integration/smartcontracts/mint_rose_trigger/src/lib.rs @@ -17,7 +17,7 @@ getrandom::register_custom_getrandom!(iroha_trigger::stub_getrandom); /// Mint 1 rose for owner #[iroha_trigger::main] -fn main(owner: AccountId, _event: Event) { +fn main(owner: AccountId, _event: EventBox) { let rose_definition_id = AssetDefinitionId::from_str("rose#wonderland") .dbg_expect("Failed to parse `rose#wonderland` asset definition id"); let rose_id = AssetId::new(rose_definition_id, owner); diff --git a/client/tests/integration/smartcontracts/query_assets_and_save_cursor/src/lib.rs b/client/tests/integration/smartcontracts/query_assets_and_save_cursor/src/lib.rs index 5028ca4e01d..feadea44447 100644 --- a/client/tests/integration/smartcontracts/query_assets_and_save_cursor/src/lib.rs +++ b/client/tests/integration/smartcontracts/query_assets_and_save_cursor/src/lib.rs @@ -26,7 +26,7 @@ fn main(owner: AccountId) { .execute() .dbg_unwrap(); - let (_batch, cursor) = asset_cursor.into_raw_parts(); + let (_batch, cursor) = asset_cursor.into_parts(); SetKeyValue::account( owner, diff --git a/client/tests/integration/triggers/by_call_trigger.rs b/client/tests/integration/triggers/by_call_trigger.rs index a2c2ac2b41d..ff76caf34f2 100644 --- a/client/tests/integration/triggers/by_call_trigger.rs +++ b/client/tests/integration/triggers/by_call_trigger.rs @@ -58,11 +58,9 @@ fn execute_trigger_should_produce_event() -> Result<()> { let thread_client = test_client.clone(); let (sender, receiver) = mpsc::channel(); let _handle = thread::spawn(move || -> Result<()> { - let mut event_it = thread_client.listen_for_events( - ExecuteTriggerEventFilter::new() - .for_trigger(trigger_id) - .under_authority(account_id), - )?; + let mut event_it = thread_client.listen_for_events([ExecuteTriggerEventFilter::new() + .for_trigger(trigger_id) + .under_authority(account_id)])?; if event_it.next().is_some() { sender.send(())?; return Ok(()); diff --git a/client/tests/integration/triggers/time_trigger.rs b/client/tests/integration/triggers/time_trigger.rs index 1f29a0d8ba9..8c335d894b3 100644 --- a/client/tests/integration/triggers/time_trigger.rs +++ b/client/tests/integration/triggers/time_trigger.rs @@ -5,12 +5,30 @@ use iroha_client::{ client::{self, Client, QueryResult}, data_model::{prelude::*, transaction::WasmSmartContract}, }; -use iroha_config::parameters::defaults::chain_wide::DEFAULT_CONSENSUS_ESTIMATION; +use iroha_config::parameters::defaults::chain_wide::{DEFAULT_BLOCK_TIME, DEFAULT_COMMIT_TIME}; +use iroha_data_model::events::pipeline::{BlockEventFilter, BlockStatus}; use iroha_logger::info; use test_network::*; use crate::integration::new_account_with_random_public_key; +const DEFAULT_CONSENSUS_ESTIMATION: Duration = + match DEFAULT_BLOCK_TIME.checked_add(match DEFAULT_COMMIT_TIME.checked_div(2) { + Some(x) => x, + None => unreachable!(), + }) { + Some(x) => x, + None => unreachable!(), + }; + +fn curr_time() -> core::time::Duration { + use std::time::SystemTime; + + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Failed to get the current system time") +} + /// Macro to abort compilation, if `e` isn't `true` macro_rules! const_assert { ($e:expr) => { @@ -33,7 +51,7 @@ fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result let (_rt, _peer, mut test_client) = ::new().with_port(10_775).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); - let start_time = current_time(); + let start_time = curr_time(); // Start listening BEFORE submitting any transaction not to miss any block committed event let event_listener = get_block_committed_event_listener(&test_client)?; @@ -66,7 +84,7 @@ fn time_trigger_execution_count_error_should_be_less_than_15_percent() -> Result )?; std::thread::sleep(DEFAULT_CONSENSUS_ESTIMATION); - let finish_time = current_time(); + let finish_time = curr_time(); let average_count = finish_time.saturating_sub(start_time).as_millis() / PERIOD.as_millis(); let actual_value = get_asset_value(&mut test_client, asset_id); @@ -92,7 +110,7 @@ fn change_asset_metadata_after_1_sec() -> Result<()> { let (_rt, _peer, mut test_client) = ::new().with_port(10_660).start_with_runtime(); wait_for_genesis_committed(&vec![test_client.clone()], 0); - let start_time = current_time(); + let start_time = curr_time(); // Start listening BEFORE submitting any transaction not to miss any block committed event let event_listener = get_block_committed_event_listener(&test_client)?; @@ -220,7 +238,7 @@ fn mint_nft_for_every_user_every_1_sec() -> Result<()> { let event_listener = get_block_committed_event_listener(&test_client)?; // Registering trigger - let start_time = current_time(); + let start_time = curr_time(); let schedule = TimeSchedule::starting_at(start_time).with_period(Duration::from_millis(TRIGGER_PERIOD_MS)); let register_trigger = Register::trigger(Trigger::new( @@ -272,11 +290,9 @@ fn mint_nft_for_every_user_every_1_sec() -> Result<()> { /// Get block committed event listener fn get_block_committed_event_listener( client: &Client, -) -> Result>> { - let block_filter = PipelineEventFilter::new() - .for_entity(PipelineEntityKind::Block) - .for_status(PipelineStatusKind::Committed); - client.listen_for_events(block_filter) +) -> Result>> { + let block_filter = BlockEventFilter::default().for_status(BlockStatus::Committed); + client.listen_for_events([block_filter]) } /// Get asset numeric value @@ -292,7 +308,7 @@ fn get_asset_value(client: &mut Client, asset_id: AssetId) -> Numeric { /// Submit some sample ISIs to create new blocks fn submit_sample_isi_on_every_block_commit( - block_committed_event_listener: impl Iterator>, + block_committed_event_listener: impl Iterator>, test_client: &mut Client, account_id: &AccountId, timeout: Duration, diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 807d504a280..7a817316e57 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -249,13 +249,17 @@ mod filter { mod events { + use iroha_client::data_model::events::pipeline::{BlockEventFilter, TransactionEventFilter}; + use super::*; /// Get event stream from iroha peer #[derive(clap::Subcommand, Debug, Clone, Copy)] pub enum Args { - /// Gets pipeline events - Pipeline, + /// Gets block pipeline events + BlockPipeline, + /// Gets transaction pipeline events + TransactionPipeline, /// Gets data events Data, /// Get execute trigger events @@ -267,7 +271,8 @@ mod events { impl RunArgs for Args { fn run(self, context: &mut dyn RunContext) -> Result<()> { match self { - Args::Pipeline => listen(PipelineEventFilter::new(), context), + Args::TransactionPipeline => listen(TransactionEventFilter::default(), context), + Args::BlockPipeline => listen(BlockEventFilter::default(), context), Args::Data => listen(DataEventFilter::Any, context), Args::ExecuteTrigger => listen(ExecuteTriggerEventFilter::new(), context), Args::TriggerCompleted => listen(TriggerCompletedEventFilter::new(), context), @@ -280,7 +285,7 @@ mod events { let iroha_client = context.client_from_config(); eprintln!("Listening to events with filter: {filter:?}"); iroha_client - .listen_for_events(filter) + .listen_for_events([filter]) .wrap_err("Failed to listen for events.")? .try_for_each(|event| context.print_data(&event?))?; Ok(()) diff --git a/config/Cargo.toml b/config/Cargo.toml index 8c354f4372b..8daf3edca93 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -44,4 +44,4 @@ trybuild = { workspace = true } hex = { workspace = true } [features] -tokio-console = [] +tokio_console = [] diff --git a/config/src/parameters/defaults.rs b/config/src/parameters/defaults.rs index ff55704ee09..a212d4dfb56 100644 --- a/config/src/parameters/defaults.rs +++ b/config/src/parameters/defaults.rs @@ -24,7 +24,7 @@ pub mod kura { pub const DEFAULT_STORE_DIR: &str = "./storage"; } -#[cfg(feature = "tokio-console")] +#[cfg(feature = "tokio_console")] pub mod logger { use iroha_primitives::addr::{socket_addr, SocketAddr}; @@ -61,16 +61,6 @@ pub mod chain_wide { // TODO: wrap into a `Bytes` newtype pub const DEFAULT_WASM_MAX_MEMORY_BYTES: u32 = 500 * 2_u32.pow(20); - /// Default estimation of consensus duration. - pub const DEFAULT_CONSENSUS_ESTIMATION: Duration = - match DEFAULT_BLOCK_TIME.checked_add(match DEFAULT_COMMIT_TIME.checked_div(2) { - Some(x) => x, - None => unreachable!(), - }) { - Some(x) => x, - None => unreachable!(), - }; - /// Default limits for metadata pub const DEFAULT_METADATA_LIMITS: MetadataLimits = MetadataLimits::new(2_u32.pow(20), 2_u32.pow(12)); diff --git a/config/src/parameters/user.rs b/config/src/parameters/user.rs index d395f50fbb6..9fc5f3255d4 100644 --- a/config/src/parameters/user.rs +++ b/config/src/parameters/user.rs @@ -461,7 +461,7 @@ pub struct Queue { pub future_threshold: Duration, } -#[allow(missing_copy_implementations)] // triggered without tokio-console +#[allow(missing_copy_implementations)] // triggered without tokio_console #[derive(Debug, Clone)] pub struct Logger { /// Level of logging verbosity @@ -471,18 +471,18 @@ pub struct Logger { pub level: Level, /// Output format pub format: LoggerFormat, - #[cfg(feature = "tokio-console")] - /// Address of tokio console (only available under "tokio-console" feature) + #[cfg(feature = "tokio_console")] + /// Address of tokio console (only available under "tokio_console" feature) pub tokio_console_address: SocketAddr, } -#[allow(clippy::derivable_impls)] // triggers in absence of `tokio-console` feature +#[allow(clippy::derivable_impls)] // triggers in absence of `tokio_console` feature impl Default for Logger { fn default() -> Self { Self { level: Level::default(), format: LoggerFormat::default(), - #[cfg(feature = "tokio-console")] + #[cfg(feature = "tokio_console")] tokio_console_address: super::defaults::logger::DEFAULT_TOKIO_CONSOLE_ADDR, } } diff --git a/config/src/parameters/user/boilerplate.rs b/config/src/parameters/user/boilerplate.rs index 34474918934..922ae429ce8 100644 --- a/config/src/parameters/user/boilerplate.rs +++ b/config/src/parameters/user/boilerplate.rs @@ -502,7 +502,7 @@ impl FromEnvDefaultFallback for QueuePartial {} /// 'Logger' configuration. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Merge)] -// `tokio_console_addr` is not `Copy`, but warning appears without `tokio-console` feature +// `tokio_console_addr` is not `Copy`, but warning appears without `tokio_console` feature #[allow(missing_copy_implementations)] #[serde(deny_unknown_fields, default)] pub struct LoggerPartial { @@ -510,8 +510,8 @@ pub struct LoggerPartial { pub level: UserField, /// Output format pub format: UserField, - #[cfg(feature = "tokio-console")] - /// Address of tokio console (only available under "tokio-console" feature) + #[cfg(feature = "tokio_console")] + /// Address of tokio console (only available under "tokio_console" feature) pub tokio_console_address: UserField, } @@ -522,7 +522,7 @@ impl UnwrapPartial for LoggerPartial { Ok(Logger { level: self.level.unwrap_or_default(), format: self.format.unwrap_or_default(), - #[cfg(feature = "tokio-console")] + #[cfg(feature = "tokio_console")] tokio_console_address: self.tokio_console_address.get().unwrap_or_else(|| { super::super::defaults::logger::DEFAULT_TOKIO_CONSOLE_ADDR.clone() }), diff --git a/config/tests/fixtures/full.toml b/config/tests/fixtures/full.toml index f38ad0e38ef..dedb06adae0 100644 --- a/config/tests/fixtures/full.toml +++ b/config/tests/fixtures/full.toml @@ -58,7 +58,7 @@ min_retry_period = 5_000 max_retry_delay_exponent = 4 [telemetry.dev] -out_file = "./dev-telemetry.json5" +out_file = "./dev_telemetry.json5" [chain_wide] max_transactions_in_block = 512 diff --git a/configs/peer.template.toml b/configs/peer.template.toml index bc01942940e..68cb6c63e70 100644 --- a/configs/peer.template.toml +++ b/configs/peer.template.toml @@ -64,4 +64,4 @@ [telemetry.dev] ## FIXME: is it JSON5? -# out_file = "./dev-telemetry.json5" +# out_file = "./dev_telemetry.json5" diff --git a/configs/swarm/executor.wasm b/configs/swarm/executor.wasm index fb05db1652ebebd21b800200616392a24d65d64b..15dd310a9d86933ecd5405c3baf6068a1609f0bd 100644 GIT binary patch delta 137628 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 r@7?PD=T~vX?fR>@KwRczDUZ6B8YbOXAB@UqTOp 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 diff --git a/core/Cargo.toml b/core/Cargo.toml index 17d88b76cd3..798cb092590 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -26,9 +26,7 @@ telemetry = [] cli = [] # Support developer-specific telemetry. # Should not be enabled on production builds. -dev-telemetry = ["telemetry", "iroha_telemetry/dev-telemetry"] -# Support Prometheus metrics. See https://prometheus.io/. -expensive-telemetry = ["iroha_telemetry/metric-instrumentation"] +dev_telemetry = ["telemetry", "iroha_telemetry/dev_telemetry"] # Profiler integration for wasmtime profiling = [] @@ -109,8 +107,8 @@ path = "benches/blocks/validate_blocks_oneshot.rs" [package.metadata.cargo-all-features] denylist = [ -"schema-endpoint", +"schema_endpoint", "telemetry", -"test-network" +"test_network" ] skip_optional_dependencies = true diff --git a/core/benches/blocks/apply_blocks.rs b/core/benches/blocks/apply_blocks.rs index f85921695d5..104f6728dca 100644 --- a/core/benches/blocks/apply_blocks.rs +++ b/core/benches/blocks/apply_blocks.rs @@ -39,7 +39,7 @@ impl StateApplyBlocks { let state = build_state(rt, &account_id, &key_pair); instructions .into_iter() - .map(|instructions| -> Result<_> { + .map(|instructions| { let mut state_block = state.block(); let block = create_block( &mut state_block, @@ -47,11 +47,11 @@ impl StateApplyBlocks { account_id.clone(), &key_pair, ); - state_block.apply_without_execution(&block)?; + let _wsv_events = state_block.apply_without_execution(&block); state_block.commit(); - Ok(block) + block }) - .collect::, _>>()? + .collect::>() }; Ok(Self { state, blocks }) diff --git a/core/benches/blocks/common.rs b/core/benches/blocks/common.rs index e4070b458c5..d88514f7c9f 100644 --- a/core/benches/blocks/common.rs +++ b/core/benches/blocks/common.rs @@ -42,7 +42,9 @@ pub fn create_block( ) .chain(0, state) .sign(key_pair) + .unpack(|_| {}) .commit(&topology) + .unpack(|_| {}) .unwrap(); // Verify that transactions are valid diff --git a/core/benches/blocks/validate_blocks.rs b/core/benches/blocks/validate_blocks.rs index 3390d7aaebe..6aa027d5f65 100644 --- a/core/benches/blocks/validate_blocks.rs +++ b/core/benches/blocks/validate_blocks.rs @@ -1,4 +1,3 @@ -use eyre::Result; use iroha_core::{prelude::*, state::State}; use iroha_data_model::{isi::InstructionBox, prelude::*}; @@ -21,11 +20,11 @@ impl StateValidateBlocks { /// - Failed to parse [`AccountId`] /// - Failed to generate [`KeyPair`] /// - Failed to create instructions for block - pub fn setup(rt: &tokio::runtime::Handle) -> Result { + pub fn setup(rt: &tokio::runtime::Handle) -> Self { let domains = 100; let accounts_per_domain = 1000; let assets_per_domain = 1000; - let account_id: AccountId = "alice@wonderland".parse()?; + let account_id: AccountId = "alice@wonderland".parse().unwrap(); let key_pair = KeyPair::random(); let state = build_state(rt, &account_id, &key_pair); @@ -38,12 +37,12 @@ impl StateValidateBlocks { .into_iter() .collect::>(); - Ok(Self { + Self { state, instructions, key_pair, account_id, - }) + } } /// Run benchmark body. @@ -61,7 +60,7 @@ impl StateValidateBlocks { key_pair, account_id, }: Self, - ) -> Result<()> { + ) { for (instructions, i) in instructions.into_iter().zip(1..) { let mut state_block = state.block(); let block = create_block( @@ -70,11 +69,9 @@ impl StateValidateBlocks { account_id.clone(), &key_pair, ); - state_block.apply_without_execution(&block)?; + let _wsv_events = state_block.apply_without_execution(&block); assert_eq!(state_block.height(), i); state_block.commit(); } - - Ok(()) } } diff --git a/core/benches/blocks/validate_blocks_benchmark.rs b/core/benches/blocks/validate_blocks_benchmark.rs index 454e07e3f4c..c3592b506f2 100644 --- a/core/benches/blocks/validate_blocks_benchmark.rs +++ b/core/benches/blocks/validate_blocks_benchmark.rs @@ -15,10 +15,8 @@ fn validate_blocks(c: &mut Criterion) { group.significance_level(0.1).sample_size(10); group.bench_function("validate_blocks", |b| { b.iter_batched( - || StateValidateBlocks::setup(rt.handle()).expect("Failed to setup benchmark"), - |bench| { - StateValidateBlocks::measure(bench).expect("Failed to execute benchmark"); - }, + || StateValidateBlocks::setup(rt.handle()), + StateValidateBlocks::measure, criterion::BatchSize::SmallInput, ); }); diff --git a/core/benches/blocks/validate_blocks_oneshot.rs b/core/benches/blocks/validate_blocks_oneshot.rs index 118ce739b99..8c8b20b1343 100644 --- a/core/benches/blocks/validate_blocks_oneshot.rs +++ b/core/benches/blocks/validate_blocks_oneshot.rs @@ -20,6 +20,6 @@ fn main() { } iroha_logger::test_logger(); iroha_logger::info!("Starting..."); - let bench = StateValidateBlocks::setup(rt.handle()).expect("Failed to setup benchmark"); - StateValidateBlocks::measure(bench).expect("Failed to execute bnechmark"); + let bench = StateValidateBlocks::setup(rt.handle()); + StateValidateBlocks::measure(bench); } diff --git a/core/benches/kura.rs b/core/benches/kura.rs index 06f78dcfc9b..521e242f60e 100644 --- a/core/benches/kura.rs +++ b/core/benches/kura.rs @@ -56,6 +56,7 @@ async fn measure_block_size_for_n_executors(n_executors: u32) { BlockBuilder::new(vec![tx], topology, Vec::new()) .chain(0, &mut state_block) .sign(&KeyPair::random()) + .unpack(|_| {}) }; for _ in 1..n_executors { diff --git a/core/benches/validation.rs b/core/benches/validation.rs index 8aff8c01ce0..d7e5459f090 100644 --- a/core/benches/validation.rs +++ b/core/benches/validation.rs @@ -186,7 +186,7 @@ fn sign_blocks(criterion: &mut Criterion) { b.iter_batched( || block.clone(), |block| { - let _: ValidBlock = block.sign(&key_pair); + let _: ValidBlock = block.sign(&key_pair).unpack(|_| {}); count += 1; }, BatchSize::SmallInput, diff --git a/core/src/block.rs b/core/src/block.rs index 4a6f210502e..3134f1109b0 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -6,7 +6,6 @@ //! [`Block`]s are organised into a linear sequence over time (also known as the block chain). use std::error::Error as _; -use iroha_config::parameters::defaults::chain_wide::DEFAULT_CONSENSUS_ESTIMATION; use iroha_crypto::{HashOf, KeyPair, MerkleTree, SignatureOf, SignaturesOf}; use iroha_data_model::{ block::*, @@ -18,6 +17,7 @@ use iroha_genesis::GenesisTransaction; use iroha_primitives::unique_vec::UniqueVec; use thiserror::Error; +pub(crate) use self::event::WithEvents; pub use self::{chained::Chained, commit::CommittedBlock, valid::ValidBlock}; use crate::{prelude::*, sumeragi::network_topology::Topology, tx::AcceptTransactionFail}; @@ -37,20 +37,27 @@ pub enum TransactionValidationError { pub enum BlockValidationError { /// Block has committed transactions HasCommittedTransactions, - /// Mismatch between the actual and expected hashes of the latest block. Expected: {expected:?}, actual: {actual:?} - LatestBlockHashMismatch { + /// Mismatch between the actual and expected hashes of the previous block. Expected: {expected:?}, actual: {actual:?} + PrevBlockHashMismatch { /// Expected value expected: Option>, /// Actual value actual: Option>, }, - /// Mismatch between the actual and expected height of the latest block. Expected: {expected}, actual: {actual} - LatestBlockHeightMismatch { + /// Mismatch between the actual and expected height of the previous block. Expected: {expected}, actual: {actual} + PrevBlockHeightMismatch { /// Expected value expected: u64, /// Actual value actual: u64, }, + /// Mismatch between the actual and expected hashes of the current block. Expected: {expected:?}, actual: {actual:?} + IncorrectHash { + /// Expected value + expected: HashOf, + /// Actual value + actual: HashOf, + }, /// The transaction hash stored in the block header does not match the actual transaction hash TransactionHashMismatch, /// Error during transaction validation @@ -93,6 +100,8 @@ pub enum SignatureVerificationError { pub struct BlockBuilder(B); mod pending { + use std::time::SystemTime; + use iroha_data_model::transaction::TransactionValue; use super::*; @@ -110,7 +119,7 @@ mod pending { /// Transaction will be validated when block is chained. transactions: Vec, /// Event recommendations for use in triggers and off-chain work - event_recommendations: Vec, + event_recommendations: Vec, } impl BlockBuilder { @@ -123,7 +132,7 @@ mod pending { pub fn new( transactions: Vec, commit_topology: Topology, - event_recommendations: Vec, + event_recommendations: Vec, ) -> Self { assert!(!transactions.is_empty(), "Empty block created"); @@ -136,27 +145,26 @@ mod pending { fn make_header( previous_height: u64, - previous_block_hash: Option>, + prev_block_hash: Option>, view_change_index: u64, transactions: &[TransactionValue], ) -> BlockHeader { BlockHeader { - timestamp_ms: iroha_data_model::current_time() - .as_millis() - .try_into() - .expect("Time should fit into u64"), - consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION + creation_time_ms: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Failed to get the current system time") .as_millis() .try_into() .expect("Time should fit into u64"), height: previous_height + 1, view_change_index, - previous_block_hash, + prev_block_hash, transactions_hash: transactions .iter() .map(|value| value.as_ref().hash()) .collect::>() - .hash(), + .hash() + .unwrap(), } } @@ -222,16 +230,16 @@ mod chained { impl BlockBuilder { /// Sign this block and get [`SignedBlock`]. - pub fn sign(self, key_pair: &KeyPair) -> ValidBlock { + pub fn sign(self, key_pair: &KeyPair) -> WithEvents { let signature = SignatureOf::new(key_pair, &self.0 .0); - ValidBlock( + WithEvents::new(ValidBlock( SignedBlockV1 { payload: self.0 .0, signatures: SignaturesOf::from(signature), } .into(), - ) + )) } } } @@ -245,7 +253,7 @@ mod valid { /// Block that was validated and accepted #[derive(Debug, Clone)] #[repr(transparent)] - pub struct ValidBlock(pub(crate) SignedBlock); + pub struct ValidBlock(pub(super) SignedBlock); impl ValidBlock { /// Validate a block against the current state of the world. @@ -264,7 +272,7 @@ mod valid { topology: &Topology, expected_chain_id: &ChainId, state_block: &mut StateBlock<'_>, - ) -> Result { + ) -> WithEvents> { if !block.header().is_genesis() { let actual_commit_topology = block.commit_topology(); let expected_commit_topology = &topology.ordered_peers; @@ -272,20 +280,23 @@ mod valid { if actual_commit_topology != expected_commit_topology { let actual_commit_topology = actual_commit_topology.clone(); - return Err(( + return WithEvents::new(Err(( block, BlockValidationError::TopologyMismatch { expected: expected_commit_topology.clone(), actual: actual_commit_topology, }, - )); + ))); } if topology .filter_signatures_by_roles(&[Role::Leader], block.signatures()) .is_empty() { - return Err((block, SignatureVerificationError::LeaderMissing.into())); + return WithEvents::new(Err(( + block, + SignatureVerificationError::LeaderMissing.into(), + ))); } } @@ -293,48 +304,51 @@ mod valid { let actual_height = block.header().height; if expected_block_height != actual_height { - return Err(( + return WithEvents::new(Err(( block, - BlockValidationError::LatestBlockHeightMismatch { + BlockValidationError::PrevBlockHeightMismatch { expected: expected_block_height, actual: actual_height, }, - )); + ))); } - let expected_previous_block_hash = state_block.latest_block_hash(); - let actual_block_hash = block.header().previous_block_hash; + let expected_prev_block_hash = state_block.latest_block_hash(); + let actual_prev_block_hash = block.header().prev_block_hash; - if expected_previous_block_hash != actual_block_hash { - return Err(( + if expected_prev_block_hash != actual_prev_block_hash { + return WithEvents::new(Err(( block, - BlockValidationError::LatestBlockHashMismatch { - expected: expected_previous_block_hash, - actual: actual_block_hash, + BlockValidationError::PrevBlockHashMismatch { + expected: expected_prev_block_hash, + actual: actual_prev_block_hash, }, - )); + ))); } if block .transactions() .any(|tx| state_block.has_transaction(tx.as_ref().hash())) { - return Err((block, BlockValidationError::HasCommittedTransactions)); + return WithEvents::new(Err(( + block, + BlockValidationError::HasCommittedTransactions, + ))); } if let Err(error) = Self::validate_transactions(&block, expected_chain_id, state_block) { - return Err((block, error.into())); + return WithEvents::new(Err((block, error.into()))); } let SignedBlock::V1(block) = block; - Ok(ValidBlock( + WithEvents::new(Ok(ValidBlock( SignedBlockV1 { payload: block.payload, signatures: block.signatures, } .into(), - )) + ))) } fn validate_transactions( @@ -379,24 +393,44 @@ mod valid { /// /// - Not enough signatures /// - Not signed by proxy tail - pub(crate) fn commit_with_signatures( + pub fn commit_with_signatures( mut self, topology: &Topology, signatures: SignaturesOf, - ) -> Result { + expected_hash: HashOf, + ) -> WithEvents> { if topology .filter_signatures_by_roles(&[Role::Leader], &signatures) .is_empty() { - return Err((self, SignatureVerificationError::LeaderMissing.into())); + return WithEvents::new(Err(( + self, + SignatureVerificationError::LeaderMissing.into(), + ))); } if !self.as_ref().signatures().is_subset(&signatures) { - return Err((self, SignatureVerificationError::SignatureMissing.into())); + return WithEvents::new(Err(( + self, + SignatureVerificationError::SignatureMissing.into(), + ))); } if !self.0.replace_signatures(signatures) { - return Err((self, SignatureVerificationError::UnknownSignature.into())); + return WithEvents::new(Err(( + self, + SignatureVerificationError::UnknownSignature.into(), + ))); + } + + let actual_block_hash = self.as_ref().hash(); + if actual_block_hash != expected_hash { + let err = BlockValidationError::IncorrectHash { + expected: expected_hash, + actual: actual_block_hash, + }; + + return WithEvents::new(Err((self, err))); } self.commit(topology) @@ -411,19 +445,19 @@ mod valid { pub fn commit( self, topology: &Topology, - ) -> Result { + ) -> WithEvents> { if !self.0.header().is_genesis() { if let Err(err) = self.verify_signatures(topology) { - return Err((self, err.into())); + return WithEvents::new(Err((self, err.into()))); } } - Ok(CommittedBlock(self)) + WithEvents::new(Ok(CommittedBlock(self))) } /// Add additional signatures for [`Self`]. #[must_use] - pub fn sign(self, key_pair: &KeyPair) -> Self { + pub fn sign(self, key_pair: &KeyPair) -> ValidBlock { ValidBlock(self.0.sign(key_pair)) } @@ -443,21 +477,20 @@ mod valid { pub(crate) fn new_dummy() -> Self { BlockBuilder(Chained(BlockPayload { header: BlockHeader { - timestamp_ms: 0, - consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION - .as_millis() - .try_into() - .expect("Should never overflow?"), height: 2, + creation_time_ms: 0, + prev_block_hash: None, + transactions_hash: HashOf::from_untyped_unchecked(Hash::prehashed( + [0_u8; Hash::LENGTH], + )), view_change_index: 0, - previous_block_hash: None, - transactions_hash: None, }, transactions: Vec::new(), commit_topology: UniqueVec::new(), event_recommendations: Vec::new(), })) .sign(&KeyPair::random()) + .unpack(|_| {}) } /// Check if block's signatures meet requirements for given topology. @@ -628,31 +661,7 @@ mod commit { /// Represents a block accepted by consensus. /// Every [`Self`] will have a different height. #[derive(Debug, Clone)] - pub struct CommittedBlock(pub(crate) ValidBlock); - - impl CommittedBlock { - pub(crate) fn produce_events(&self) -> Vec { - let tx = self.as_ref().transactions().map(|tx| { - let status = tx.error.as_ref().map_or_else( - || PipelineStatus::Committed, - |error| PipelineStatus::Rejected(error.clone().into()), - ); - - PipelineEvent { - entity_kind: PipelineEntityKind::Transaction, - status, - hash: tx.as_ref().hash().into(), - } - }); - let current_block = core::iter::once(PipelineEvent { - entity_kind: PipelineEntityKind::Block, - status: PipelineStatus::Committed, - hash: self.as_ref().hash().into(), - }); - - tx.chain(current_block).collect() - } - } + pub struct CommittedBlock(pub(super) ValidBlock); impl From for ValidBlock { fn from(source: CommittedBlock) -> Self { @@ -666,12 +675,105 @@ mod commit { } } - // Invariants of [`CommittedBlock`] can't be violated through immutable reference impl AsRef for CommittedBlock { fn as_ref(&self) -> &SignedBlock { &self.0 .0 } } + + #[cfg(test)] + impl AsMut for CommittedBlock { + fn as_mut(&mut self) -> &mut SignedBlock { + &mut self.0 .0 + } + } +} + +mod event { + use super::*; + + pub trait EventProducer { + fn produce_events(&self) -> impl Iterator; + } + + #[derive(Debug)] + #[must_use] + pub struct WithEvents(B); + + impl WithEvents { + pub(super) fn new(source: B) -> Self { + Self(source) + } + } + + impl WithEvents> { + pub fn unpack(self, f: F) -> Result { + match self.0 { + Ok(ok) => Ok(WithEvents(ok).unpack(f)), + Err(err) => Err(WithEvents(err).unpack(f)), + } + } + } + impl WithEvents { + pub fn unpack(self, f: F) -> B { + self.0.produce_events().for_each(f); + self.0 + } + } + + impl WithEvents<(B, E)> { + pub(crate) fn unpack(self, f: F) -> (B, E) { + self.0 .1.produce_events().for_each(f); + self.0 + } + } + + impl EventProducer for ValidBlock { + fn produce_events(&self) -> impl Iterator { + let block_height = self.as_ref().header().height; + + let tx_events = self.as_ref().transactions().map(move |tx| { + let status = tx.error.as_ref().map_or_else( + || TransactionStatus::Approved, + |error| TransactionStatus::Rejected(error.clone().into()), + ); + + TransactionEvent { + block_height: Some(block_height), + hash: tx.as_ref().hash(), + status, + } + }); + + let block_event = core::iter::once(BlockEvent { + header: self.as_ref().header().clone(), + hash: self.as_ref().hash(), + status: BlockStatus::Approved, + }); + + tx_events + .map(PipelineEventBox::from) + .chain(block_event.map(Into::into)) + } + } + + impl EventProducer for CommittedBlock { + fn produce_events(&self) -> impl Iterator { + let block_event = core::iter::once(BlockEvent { + header: self.as_ref().header().clone(), + hash: self.as_ref().hash(), + status: BlockStatus::Committed, + }); + + block_event.map(Into::into) + } + } + + impl EventProducer for BlockValidationError { + fn produce_events(&self) -> impl Iterator { + core::iter::empty() + } + } } #[cfg(test)] @@ -690,12 +792,13 @@ mod tests { pub fn committed_and_valid_block_hashes_are_equal() { let valid_block = ValidBlock::new_dummy(); let topology = Topology::new(UniqueVec::new()); - let committed_block = valid_block.clone().commit(&topology).unwrap(); + let committed_block = valid_block + .clone() + .commit(&topology) + .unpack(|_| {}) + .unwrap(); - assert_eq!( - valid_block.0.hash_of_payload(), - committed_block.as_ref().hash_of_payload() - ) + assert_eq!(valid_block.0.hash(), committed_block.as_ref().hash()) } #[tokio::test] @@ -733,13 +836,26 @@ mod tests { let topology = Topology::new(UniqueVec::new()); let valid_block = BlockBuilder::new(transactions, topology, Vec::new()) .chain(0, &mut state_block) - .sign(&alice_keys); + .sign(&alice_keys) + .unpack(|_| {}); // The first transaction should be confirmed - assert!(valid_block.0.transactions().next().unwrap().error.is_none()); + assert!(valid_block + .as_ref() + .transactions() + .next() + .unwrap() + .error + .is_none()); // The second transaction should be rejected - assert!(valid_block.0.transactions().nth(1).unwrap().error.is_some()); + assert!(valid_block + .as_ref() + .transactions() + .nth(1) + .unwrap() + .error + .is_some()); } #[tokio::test] @@ -795,13 +911,26 @@ mod tests { let topology = Topology::new(UniqueVec::new()); let valid_block = BlockBuilder::new(transactions, topology, Vec::new()) .chain(0, &mut state_block) - .sign(&alice_keys); + .sign(&alice_keys) + .unpack(|_| {}); // The first transaction should fail - assert!(valid_block.0.transactions().next().unwrap().error.is_some()); + assert!(valid_block + .as_ref() + .transactions() + .next() + .unwrap() + .error + .is_some()); // The third transaction should succeed - assert!(valid_block.0.transactions().nth(2).unwrap().error.is_none()); + assert!(valid_block + .as_ref() + .transactions() + .nth(2) + .unwrap() + .error + .is_none()); } #[tokio::test] @@ -852,17 +981,30 @@ mod tests { let topology = Topology::new(UniqueVec::new()); let valid_block = BlockBuilder::new(transactions, topology, Vec::new()) .chain(0, &mut state_block) - .sign(&alice_keys); + .sign(&alice_keys) + .unpack(|_| {}); // The first transaction should be rejected assert!( - valid_block.0.transactions().next().unwrap().error.is_some(), + valid_block + .as_ref() + .transactions() + .next() + .unwrap() + .error + .is_some(), "The first transaction should be rejected, as it contains `Fail`." ); // The second transaction should be accepted assert!( - valid_block.0.transactions().nth(1).unwrap().error.is_none(), + valid_block + .as_ref() + .transactions() + .nth(1) + .unwrap() + .error + .is_none(), "The second transaction should be accepted." ); } diff --git a/core/src/block_sync.rs b/core/src/block_sync.rs index d2e5c6b7219..ef7f5b8c10a 100644 --- a/core/src/block_sync.rs +++ b/core/src/block_sync.rs @@ -91,16 +91,13 @@ impl BlockSynchronizer { /// Sends request for latest blocks to a chosen peer async fn request_latest_blocks_from_peer(&mut self, peer_id: PeerId) { - let (previous_hash, latest_hash) = { + let (prev_hash, latest_hash) = { let state_view = self.state.view(); - ( - state_view.previous_block_hash(), - state_view.latest_block_hash(), - ) + (state_view.prev_block_hash(), state_view.latest_block_hash()) }; message::Message::GetBlocksAfter(message::GetBlocksAfter::new( latest_hash, - previous_hash, + prev_hash, self.peer_id.clone(), )) .send_to(&self.network, peer_id) @@ -138,7 +135,7 @@ pub mod message { /// Hash of latest available block pub latest_hash: Option>, /// Hash of second to latest block - pub previous_hash: Option>, + pub prev_hash: Option>, /// Peer id pub peer_id: PeerId, } @@ -147,12 +144,12 @@ pub mod message { /// Construct [`GetBlocksAfter`]. pub const fn new( latest_hash: Option>, - previous_hash: Option>, + prev_hash: Option>, peer_id: PeerId, ) -> Self { Self { latest_hash, - previous_hash, + prev_hash, peer_id, } } @@ -190,21 +187,21 @@ pub mod message { match self { Message::GetBlocksAfter(GetBlocksAfter { latest_hash, - previous_hash, + prev_hash, peer_id, }) => { let local_latest_block_hash = block_sync.state.view().latest_block_hash(); if *latest_hash == local_latest_block_hash - || *previous_hash == local_latest_block_hash + || *prev_hash == local_latest_block_hash { return; } - let start_height = match previous_hash { + let start_height = match prev_hash { Some(hash) => match block_sync.kura.get_block_height_by_hash(hash) { None => { - error!(?previous_hash, "Block hash not found"); + error!(?prev_hash, "Block hash not found"); return; } Some(height) => height + 1, // It's get blocks *after*, so we add 1. @@ -223,9 +220,9 @@ pub mod message { // The only case where the blocks array could be empty is if we got queried for blocks // after the latest hash. There is a check earlier in the function that returns early // so it should not be possible for us to get here. - error!(hash=?previous_hash, "Blocks array is empty but shouldn't be."); + error!(hash=?prev_hash, "Blocks array is empty but shouldn't be."); } else { - trace!(hash=?previous_hash, "Sharing blocks after hash"); + trace!(hash=?prev_hash, "Sharing blocks after hash"); Message::ShareBlocks(ShareBlocks::new(blocks, block_sync.peer_id.clone())) .send_to(&block_sync.network, peer_id.clone()) .await; diff --git a/core/src/kura.rs b/core/src/kura.rs index 3dc536f9c2d..49bbf9d401a 100644 --- a/core/src/kura.rs +++ b/core/src/kura.rs @@ -154,7 +154,7 @@ impl Kura { let mut block_indices = vec![BlockIndex::default(); block_index_count]; block_store.read_block_indices(0, &mut block_indices)?; - let mut previous_block_hash = None; + let mut prev_block_hash = None; for block in block_indices { // This is re-allocated every iteration. This could cause a problem. let mut block_data_buffer = vec![0_u8; block.length.try_into()?]; @@ -162,13 +162,13 @@ impl Kura { match block_store.read_block_data(block.start, &mut block_data_buffer) { Ok(()) => match SignedBlock::decode_all_versioned(&block_data_buffer) { Ok(decoded_block) => { - if previous_block_hash != decoded_block.header().previous_block_hash { + if prev_block_hash != decoded_block.header().prev_block_hash { error!("Block has wrong previous block hash. Not reading any blocks beyond this height."); break; } let decoded_block_hash = decoded_block.hash(); block_hashes.push(decoded_block_hash); - previous_block_hash = Some(decoded_block_hash); + prev_block_hash = Some(decoded_block_hash); } Err(error) => { error!(?error, "Encountered malformed block. Not reading any blocks beyond this height."); diff --git a/core/src/lib.rs b/core/src/lib.rs index ab0b9be0d6b..06a0bd4103f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -18,7 +18,7 @@ use core::time::Duration; use gossiper::TransactionGossip; use indexmap::IndexSet; -use iroha_data_model::prelude::*; +use iroha_data_model::{events::EventBox, prelude::*}; use iroha_primitives::unique_vec::UniqueVec; use parity_scale_codec::{Decode, Encode}; use tokio::sync::broadcast; @@ -41,8 +41,8 @@ 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; +/// Type of `Sender` which should be used for channels of `Event` messages. +pub type EventsSender = broadcast::Sender; /// The network message #[derive(Clone, Debug, Encode, Decode)] diff --git a/core/src/queue.rs b/core/src/queue.rs index d463a655a4c..dc7e2ef9926 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -1,6 +1,6 @@ //! Module with queue actor use core::time::Duration; -use std::num::NonZeroUsize; +use std::{num::NonZeroUsize, time::SystemTime}; use crossbeam_queue::ArrayQueue; use dashmap::{mapref::entry::Entry, DashMap}; @@ -8,13 +8,17 @@ use eyre::Result; use indexmap::IndexSet; use iroha_config::parameters::actual::Queue as Config; use iroha_crypto::HashOf; -use iroha_data_model::{account::AccountId, transaction::prelude::*}; +use iroha_data_model::{ + account::AccountId, + events::pipeline::{TransactionEvent, TransactionStatus}, + transaction::prelude::*, +}; use iroha_logger::{trace, warn}; use iroha_primitives::must_use::MustUse; use rand::seq::IteratorRandom; use thiserror::Error; -use crate::prelude::*; +use crate::{prelude::*, EventsSender}; impl AcceptedTransaction { // TODO: We should have another type of transaction like `CheckedTransaction` in the type system? @@ -48,6 +52,7 @@ impl AcceptedTransaction { /// Multiple producers, single consumer #[derive(Debug)] pub struct Queue { + events_sender: EventsSender, /// The queue for transactions tx_hashes: ArrayQueue>, /// [`AcceptedTransaction`]s addressed by `Hash` @@ -96,8 +101,9 @@ pub struct Failure { impl Queue { /// Makes queue from configuration - pub fn from_config(cfg: Config) -> Self { + pub fn from_config(cfg: Config, events_sender: EventsSender) -> Self { Self { + events_sender, tx_hashes: ArrayQueue::new(cfg.capacity.get()), accepted_txs: DashMap::new(), txs_per_user: DashMap::new(), @@ -121,13 +127,19 @@ impl Queue { |tx_time_to_live| core::cmp::min(self.tx_time_to_live, tx_time_to_live), ); - iroha_data_model::current_time().saturating_sub(tx_creation_time) > time_limit + let curr_time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Failed to get the current system time"); + curr_time.saturating_sub(tx_creation_time) > time_limit } /// If `true`, this transaction is regarded to have been tampered to have a future timestamp. fn is_in_future(&self, tx: &AcceptedTransaction) -> bool { let tx_timestamp = tx.as_ref().creation_time(); - tx_timestamp.saturating_sub(iroha_data_model::current_time()) > self.future_threshold + let curr_time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Failed to get the current system time"); + tx_timestamp.saturating_sub(curr_time) > self.future_threshold } /// Returns all pending transactions. @@ -226,6 +238,14 @@ impl Queue { err: Error::Full, } })?; + let _ = self.events_sender.send( + TransactionEvent { + hash, + block_height: None, + status: TransactionStatus::Queued, + } + .into(), + ); trace!("Transaction queue length = {}", self.tx_hashes.len(),); Ok(()) } @@ -281,12 +301,7 @@ impl Queue { max_txs_in_block: usize, ) -> Vec { let mut transactions = Vec::with_capacity(max_txs_in_block); - self.get_transactions_for_block( - state_view, - max_txs_in_block, - &mut transactions, - &mut Vec::new(), - ); + self.get_transactions_for_block(state_view, max_txs_in_block, &mut transactions); transactions } @@ -298,17 +313,16 @@ impl Queue { state_view: &StateView, max_txs_in_block: usize, transactions: &mut Vec, - expired_transactions: &mut Vec, ) { if transactions.len() >= max_txs_in_block { return; } let mut seen_queue = Vec::new(); - let mut expired_transactions_queue = Vec::new(); + let mut expired_transactions = Vec::new(); let txs_from_queue = core::iter::from_fn(|| { - self.pop_from_queue(&mut seen_queue, state_view, &mut expired_transactions_queue) + self.pop_from_queue(&mut seen_queue, state_view, &mut expired_transactions) }); let transactions_hashes: IndexSet> = @@ -322,7 +336,17 @@ impl Queue { .into_iter() .try_for_each(|hash| self.tx_hashes.push(hash)) .expect("Exceeded the number of transactions pending"); - expired_transactions.extend(expired_transactions_queue); + + expired_transactions + .into_iter() + .map(|tx| TransactionEvent { + hash: tx.as_ref().hash(), + block_height: None, + status: TransactionStatus::Expired, + }) + .for_each(|e| { + let _ = self.events_sender.send(e.into()); + }); } /// Check that the user adhered to the maximum transaction per user limit and increment their transaction count. @@ -381,6 +405,21 @@ pub mod tests { PeersIds, }; + impl Queue { + pub fn test(cfg: Config) -> Self { + Self { + events_sender: tokio::sync::broadcast::Sender::new(1), + tx_hashes: ArrayQueue::new(cfg.capacity.get()), + accepted_txs: DashMap::new(), + txs_per_user: DashMap::new(), + capacity: cfg.capacity, + capacity_per_user: cfg.capacity_per_user, + tx_time_to_live: cfg.transaction_time_to_live, + future_threshold: cfg.future_threshold, + } + } + } + fn accepted_tx(account_id: &str, key: &KeyPair) -> AcceptedTransaction { let chain_id = ChainId::from("0"); @@ -437,7 +476,7 @@ pub mod tests { )); let state_view = state.view(); - let queue = Queue::from_config(config_factory()); + let queue = Queue::test(config_factory()); queue .push(accepted_tx("alice@wonderland", &key_pair), &state_view) @@ -458,7 +497,7 @@ pub mod tests { )); let state_view = state.view(); - let queue = Queue::from_config(Config { + let queue = Queue::test(Config { transaction_time_to_live: Duration::from_secs(100), capacity, ..Config::default() @@ -504,7 +543,7 @@ pub mod tests { }; let state_view = state.view(); - let queue = Queue::from_config(config_factory()); + let queue = Queue::test(config_factory()); let instructions: [InstructionBox; 0] = []; let tx = TransactionBuilder::new(chain_id.clone(), "alice@wonderland".parse().expect("Valid")) @@ -560,7 +599,7 @@ pub mod tests { query_handle, )); let state_view = state.view(); - let queue = Queue::from_config(Config { + let queue = Queue::test(Config { transaction_time_to_live: Duration::from_secs(100), ..config_factory() }); @@ -590,7 +629,7 @@ pub mod tests { state_block.transactions.insert(tx.as_ref().hash(), 1); state_block.commit(); let state_view = state.view(); - let queue = Queue::from_config(config_factory()); + let queue = Queue::test(config_factory()); assert!(matches!( queue.push(tx, &state_view), Err(Failure { @@ -613,7 +652,7 @@ pub mod tests { query_handle, ); let tx = accepted_tx("alice@wonderland", &alice_key); - let queue = Queue::from_config(config_factory()); + let queue = Queue::test(config_factory()); queue.push(tx.clone(), &state.view()).unwrap(); let mut state_block = state.block(); state_block.transactions.insert(tx.as_ref().hash(), 1); @@ -639,7 +678,7 @@ pub mod tests { query_handle, )); let state_view = state.view(); - let queue = Queue::from_config(Config { + let queue = Queue::test(Config { transaction_time_to_live: Duration::from_millis(300), ..config_factory() }); @@ -687,7 +726,7 @@ pub mod tests { query_handle, )); let state_view = state.view(); - let queue = Queue::from_config(config_factory()); + let queue = Queue::test(config_factory()); queue .push(accepted_tx("alice@wonderland", &alice_key), &state_view) .expect("Failed to push tx into queue"); @@ -722,7 +761,9 @@ pub mod tests { query_handle, )); let state_view = state.view(); - let queue = Queue::from_config(config_factory()); + let mut queue = Queue::test(config_factory()); + let (event_sender, mut event_receiver) = tokio::sync::broadcast::channel(1); + queue.events_sender = event_sender; let instructions = [Fail { message: "expired".to_owned(), }]; @@ -737,18 +778,39 @@ pub mod tests { max_instruction_number: 4096, max_wasm_size_bytes: 0, }; + let tx_hash = tx.hash(); let tx = AcceptedTransaction::accept(tx, &chain_id, &limits) .expect("Failed to accept Transaction."); queue .push(tx.clone(), &state_view) .expect("Failed to push tx into queue"); + let queued_tx_event = event_receiver.recv().await.unwrap(); + + assert_eq!( + queued_tx_event, + TransactionEvent { + hash: tx_hash, + block_height: None, + status: TransactionStatus::Queued, + } + .into() + ); + let mut txs = Vec::new(); - let mut expired_txs = Vec::new(); thread::sleep(Duration::from_millis(TTL_MS)); - queue.get_transactions_for_block(&state_view, max_txs_in_block, &mut txs, &mut expired_txs); + queue.get_transactions_for_block(&state_view, max_txs_in_block, &mut txs); + let expired_tx_event = event_receiver.recv().await.unwrap(); assert!(txs.is_empty()); - assert_eq!(expired_txs.len(), 1); - assert_eq!(expired_txs[0], tx); + + assert_eq!( + expired_tx_event, + TransactionEvent { + hash: tx_hash, + block_height: None, + status: TransactionStatus::Expired, + } + .into() + ) } #[test] @@ -763,7 +825,7 @@ pub mod tests { query_handle, )); - let queue = Arc::new(Queue::from_config(Config { + let queue = Arc::new(Queue::test(Config { transaction_time_to_live: Duration::from_secs(100), capacity: 100_000_000.try_into().unwrap(), ..Config::default() @@ -837,7 +899,7 @@ pub mod tests { )); let state_view = state.view(); - let queue = Queue::from_config(Config { + let queue = Queue::test(Config { future_threshold, ..Config::default() }); @@ -898,7 +960,7 @@ pub mod tests { let query_handle = LiveQueryStore::test().start(); let state = State::new(world, kura, query_handle); - let queue = Queue::from_config(Config { + let queue = Queue::test(Config { transaction_time_to_live: Duration::from_secs(100), capacity: 100.try_into().unwrap(), capacity_per_user: 1.try_into().unwrap(), diff --git a/core/src/smartcontracts/isi/query.rs b/core/src/smartcontracts/isi/query.rs index 1b8f8715ad8..e74c18ee217 100644 --- a/core/src/smartcontracts/isi/query.rs +++ b/core/src/smartcontracts/isi/query.rs @@ -316,7 +316,9 @@ mod tests { let first_block = BlockBuilder::new(transactions.clone(), topology.clone(), Vec::new()) .chain(0, &mut state_block) .sign(&ALICE_KEYS) + .unpack(|_| {}) .commit(&topology) + .unpack(|_| {}) .expect("Block is valid"); state_block.apply(&first_block)?; @@ -326,7 +328,9 @@ mod tests { let block = BlockBuilder::new(transactions.clone(), topology.clone(), Vec::new()) .chain(0, &mut state_block) .sign(&ALICE_KEYS) + .unpack(|_| {}) .commit(&topology) + .unpack(|_| {}) .expect("Block is valid"); state_block.apply(&block)?; @@ -466,7 +470,9 @@ mod tests { let vcb = BlockBuilder::new(vec![va_tx.clone()], topology.clone(), Vec::new()) .chain(0, &mut state_block) .sign(&ALICE_KEYS) + .unpack(|_| {}) .commit(&topology) + .unpack(|_| {}) .expect("Block is valid"); state_block.apply(&vcb)?; diff --git a/core/src/smartcontracts/isi/triggers/set.rs b/core/src/smartcontracts/isi/triggers/set.rs index d7bfca0b769..63d7732e92b 100644 --- a/core/src/smartcontracts/isi/triggers/set.rs +++ b/core/src/smartcontracts/isi/triggers/set.rs @@ -58,8 +58,8 @@ type WasmSmartContractMap = IndexMap, (WasmSmartContra pub struct Set { /// Triggers using [`DataEventFilter`] data_triggers: IndexMap>, - /// Triggers using [`PipelineEventFilter`] - pipeline_triggers: IndexMap>, + /// Triggers using [`PipelineEventFilterBox`] + pipeline_triggers: IndexMap>, /// Triggers using [`TimeEventFilter`] time_triggers: IndexMap>, /// Triggers using [`ExecuteTriggerEventFilter`] @@ -70,7 +70,7 @@ pub struct Set { original_contracts: WasmSmartContractMap, /// List of actions that should be triggered by events provided by `handle_*` methods. /// Vector is used to save the exact triggers order. - matched_ids: Vec<(Event, TriggerId)>, + matched_ids: Vec<(EventBox, TriggerId)>, } /// Helper struct for serializing triggers. @@ -177,7 +177,7 @@ impl<'de> DeserializeSeed<'de> for WasmSeed<'_, Set> { "pipeline_triggers" => { let triggers: IndexMap< TriggerId, - SpecializedAction, + SpecializedAction, > = map.next_value()?; for (id, action) in triggers { set.add_pipeline_trigger( @@ -259,7 +259,7 @@ impl Set { }) } - /// Add trigger with [`PipelineEventFilter`] + /// Add trigger with [`PipelineEventFilterBox`] /// /// Return `false` if a trigger with given id already exists /// @@ -270,7 +270,7 @@ impl Set { pub fn add_pipeline_trigger( &mut self, engine: &wasmtime::Engine, - trigger: SpecializedTrigger, + trigger: SpecializedTrigger, ) -> Result { self.add_to(engine, trigger, TriggeringEventType::Pipeline, |me| { &mut me.pipeline_triggers @@ -721,18 +721,6 @@ impl Set { }; } - /// Handle [`PipelineEvent`]. - /// - /// Find all actions that are triggered by `event` and store them. - /// These actions are inspected in the next [`Set::inspect_matched()`] call. - // Passing by value to follow other `handle_` methods interface - #[allow(clippy::needless_pass_by_value)] - pub fn handle_pipeline_event(&mut self, event: PipelineEvent) { - self.pipeline_triggers.iter().for_each(|entry| { - Self::match_and_insert_trigger(&mut self.matched_ids, event.clone(), entry) - }); - } - /// Handle [`TimeEvent`]. /// /// Find all actions that are triggered by `event` and store them. @@ -747,7 +735,7 @@ impl Set { continue; } - let ids = core::iter::repeat_with(|| (Event::Time(event), id.clone())).take( + let ids = core::iter::repeat_with(|| (EventBox::Time(event), id.clone())).take( count .try_into() .expect("`u32` should always fit in `usize`"), @@ -761,8 +749,8 @@ impl Set { /// Skips insertion: /// - If the action's filter doesn't match an event /// - If the action's repeats count equals to 0 - fn match_and_insert_trigger, F: EventFilter>( - matched_ids: &mut Vec<(Event, TriggerId)>, + fn match_and_insert_trigger, F: EventFilter>( + matched_ids: &mut Vec<(EventBox, TriggerId)>, event: E, (id, action): (&TriggerId, &LoadedAction), ) { @@ -825,7 +813,7 @@ impl Set { } /// Extract `matched_id` - pub fn extract_matched_ids(&mut self) -> Vec<(Event, TriggerId)> { + pub fn extract_matched_ids(&mut self) -> Vec<(EventBox, TriggerId)> { core::mem::take(&mut self.matched_ids) } } diff --git a/core/src/smartcontracts/isi/triggers/specialized.rs b/core/src/smartcontracts/isi/triggers/specialized.rs index 09e898b126d..24aa7b34500 100644 --- a/core/src/smartcontracts/isi/triggers/specialized.rs +++ b/core/src/smartcontracts/isi/triggers/specialized.rs @@ -103,7 +103,7 @@ macro_rules! impl_try_from_box { impl_try_from_box! { Data => DataEventFilter, - Pipeline => PipelineEventFilter, + Pipeline => PipelineEventFilterBox, Time => TimeEventFilter, ExecuteTrigger => ExecuteTriggerEventFilter, } @@ -228,7 +228,7 @@ mod tests { .unwrap() } TriggeringEventFilterBox::Pipeline(_) => { - SpecializedTrigger::::try_from(boxed) + SpecializedTrigger::::try_from(boxed) .map(|_| ()) .unwrap() } diff --git a/core/src/smartcontracts/wasm.rs b/core/src/smartcontracts/wasm.rs index dd8df4bd163..25f27e25675 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -465,7 +465,7 @@ pub mod state { #[derive(Constructor)] pub struct Trigger { /// Event which activated this trigger - pub(in super::super) triggering_event: Event, + pub(in super::super) triggering_event: EventBox, } pub mod executor { @@ -977,7 +977,7 @@ impl<'wrld, 'block: 'wrld, 'state: 'block> Runtime Result<()> { let span = wasm_log_span!("Trigger execution", %id, %authority); let state = state::Trigger::new( diff --git a/core/src/state.rs b/core/src/state.rs index b9291530cbf..febb38f6c20 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -7,7 +7,12 @@ use iroha_crypto::HashOf; use iroha_data_model::{ account::AccountId, block::SignedBlock, - events::trigger_completed::{TriggerCompletedEvent, TriggerCompletedOutcome}, + events::{ + pipeline::BlockEvent, + time::TimeEvent, + trigger_completed::{TriggerCompletedEvent, TriggerCompletedOutcome}, + EventBox, + }, isi::error::{InstructionExecutionError as Error, MathError}, parameter::{Parameter, ParameterValueBox}, permission::{PermissionTokenSchema, Permissions}, @@ -95,7 +100,7 @@ pub struct WorldBlock<'world> { /// Runtime Executor pub(crate) executor: CellBlock<'world, Executor>, /// Events produced during execution of block - pub(crate) events_buffer: Vec, + events_buffer: Vec, } /// Struct for single transaction's aggregated changes @@ -126,7 +131,7 @@ pub struct WorldTransaction<'block, 'world> { /// Wrapper for event's buffer to apply transaction rollback struct TransactionEventBuffer<'block> { /// Events produced during execution of block - events_buffer: &'block mut Vec, + events_buffer: &'block mut Vec, /// Number of events produced during execution current transaction events_created_in_transaction: usize, } @@ -285,7 +290,7 @@ impl World { } } - /// Create struct to apply block's changes while reverting changes made in the latest block + /// Create struct to apply block's changes while reverting changes made in the latest block pub fn block_and_revert(&self) -> WorldBlock { WorldBlock { parameters: self.parameters.block_and_revert(), @@ -895,14 +900,14 @@ impl WorldTransaction<'_, '_> { } impl TransactionEventBuffer<'_> { - fn push(&mut self, event: Event) { + fn push(&mut self, event: EventBox) { self.events_created_in_transaction += 1; self.events_buffer.push(event); } } -impl Extend for TransactionEventBuffer<'_> { - fn extend>(&mut self, iter: T) { +impl Extend for TransactionEventBuffer<'_> { + fn extend>(&mut self, iter: T) { let len_before = self.events_buffer.len(); self.events_buffer.extend(iter); let len_after = self.events_buffer.len(); @@ -1024,7 +1029,7 @@ pub trait StateReadOnly { } /// Return the hash of the block one before the latest block - fn previous_block_hash(&self) -> Option> { + fn prev_block_hash(&self) -> Option> { self.block_hashes().iter().nth_back(1).copied() } @@ -1087,7 +1092,7 @@ pub trait StateReadOnly { let opt = self .kura() .get_block_by_height(1) - .map(|genesis_block| genesis_block.header().timestamp()); + .map(|genesis_block| genesis_block.header().creation_time()); if opt.is_none() { error!("Failed to get genesis block from Kura."); @@ -1183,13 +1188,10 @@ impl<'state> StateBlock<'state> { deprecated(note = "This function is to be used in testing only. ") )] #[iroha_logger::log(skip_all, fields(block_height))] - pub fn apply(&mut self, block: &CommittedBlock) -> Result<()> { + pub fn apply(&mut self, block: &CommittedBlock) -> Result> { self.execute_transactions(block)?; debug!("All block transactions successfully executed"); - - self.apply_without_execution(block)?; - - Ok(()) + Ok(self.apply_without_execution(block)) } /// Execute `block` transactions and store their hashes as well as @@ -1217,12 +1219,12 @@ impl<'state> StateBlock<'state> { /// Apply transactions without actually executing them. /// It's assumed that block's transaction was already executed (as part of validation for example). #[iroha_logger::log(skip_all, fields(block_height = block.as_ref().header().height))] - pub fn apply_without_execution(&mut self, block: &CommittedBlock) -> Result<()> { + pub fn apply_without_execution(&mut self, block: &CommittedBlock) -> Vec { let block_hash = block.as_ref().hash(); trace!(%block_hash, "Applying block"); let time_event = self.create_time_event(block); - self.world.events_buffer.push(Event::Time(time_event)); + self.world.events_buffer.push(time_event.into()); let block_height = block.as_ref().header().height; block @@ -1248,24 +1250,44 @@ impl<'state> StateBlock<'state> { self.block_hashes.push(block_hash); self.apply_parameters(); - - Ok(()) + self.world.events_buffer.push( + BlockEvent { + header: block.as_ref().header().clone(), + hash: block.as_ref().hash(), + status: BlockStatus::Applied, + } + .into(), + ); + core::mem::take(&mut self.world.events_buffer) } /// Create time event using previous and current blocks fn create_time_event(&self, block: &CommittedBlock) -> TimeEvent { + use iroha_config::parameters::defaults::chain_wide::{ + DEFAULT_BLOCK_TIME, DEFAULT_COMMIT_TIME, + }; + + const DEFAULT_CONSENSUS_ESTIMATION: Duration = + match DEFAULT_BLOCK_TIME.checked_add(match DEFAULT_COMMIT_TIME.checked_div(2) { + Some(x) => x, + None => unreachable!(), + }) { + Some(x) => x, + None => unreachable!(), + }; + let prev_interval = self.latest_block_ref().map(|latest_block| { let header = &latest_block.as_ref().header(); TimeInterval { - since: header.timestamp(), - length: header.consensus_estimation(), + since: header.creation_time(), + length: DEFAULT_CONSENSUS_ESTIMATION, } }); let interval = TimeInterval { - since: block.as_ref().header().timestamp(), - length: block.as_ref().header().consensus_estimation(), + since: block.as_ref().header().creation_time(), + length: DEFAULT_CONSENSUS_ESTIMATION, }; TimeEvent { @@ -1388,7 +1410,7 @@ impl StateTransaction<'_, '_> { &mut self, id: &TriggerId, action: &dyn LoadedActionTrait, - event: Event, + event: EventBox, ) -> Result<()> { use triggers::set::LoadedExecutable::*; let authority = action.authority(); @@ -1751,7 +1773,7 @@ mod tests { /// Used to inject faulty payload for testing fn payload_mut(block: &mut CommittedBlock) -> &mut BlockPayload { - let SignedBlock::V1(signed) = &mut block.0 .0; + let SignedBlock::V1(signed) = block.as_mut(); &mut signed.payload } @@ -1760,7 +1782,10 @@ mod tests { const BLOCK_CNT: usize = 10; let topology = Topology::new(UniqueVec::new()); - let block = ValidBlock::new_dummy().commit(&topology).unwrap(); + let block = ValidBlock::new_dummy() + .commit(&topology) + .unpack(|_| {}) + .unwrap(); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); let state = State::new(World::default(), kura, query_handle); @@ -1771,7 +1796,7 @@ mod tests { let mut block = block.clone(); payload_mut(&mut block).header.height = i as u64; - payload_mut(&mut block).header.previous_block_hash = block_hashes.last().copied(); + payload_mut(&mut block).header.prev_block_hash = block_hashes.last().copied(); block_hashes.push(block.as_ref().hash()); state_block.apply(&block).unwrap(); @@ -1788,7 +1813,10 @@ mod tests { const BLOCK_CNT: usize = 10; let topology = Topology::new(UniqueVec::new()); - let block = ValidBlock::new_dummy().commit(&topology).unwrap(); + let block = ValidBlock::new_dummy() + .commit(&topology) + .unpack(|_| {}) + .unwrap(); let kura = Kura::blank_kura_for_testing(); let query_handle = LiveQueryStore::test().start(); let state = State::new(World::default(), kura.clone(), query_handle); @@ -1806,7 +1834,7 @@ mod tests { &state_block .all_blocks() .skip(7) - .map(|block| *block.header().height()) + .map(|block| block.header().height()) .collect::>(), &[8, 9, 10] ); diff --git a/core/src/sumeragi/main_loop.rs b/core/src/sumeragi/main_loop.rs index 13bb94bb01a..0af6b5cb5cc 100644 --- a/core/src/sumeragi/main_loop.rs +++ b/core/src/sumeragi/main_loop.rs @@ -2,10 +2,7 @@ use std::sync::mpsc; use iroha_crypto::HashOf; -use iroha_data_model::{ - block::*, events::pipeline::PipelineEvent, peer::PeerId, - transaction::error::TransactionRejectionReason, -}; +use iroha_data_model::{block::*, events::pipeline::PipelineEventBox, peer::PeerId}; use iroha_p2p::UpdateTopology; use tracing::{span, Level}; @@ -82,17 +79,19 @@ impl Sumeragi { #[allow(clippy::needless_pass_by_value, single_use_lifetimes)] // TODO: uncomment when anonymous lifetimes are stable fn broadcast_packet_to<'peer_id>( &self, - msg: BlockMessage, + msg: impl Into, ids: impl IntoIterator + Send, ) { + let msg = msg.into(); + for peer_id in ids { self.post_packet_to(msg.clone(), peer_id); } } - fn broadcast_packet(&self, msg: BlockMessage) { + fn broadcast_packet(&self, msg: impl Into) { let broadcast = iroha_p2p::Broadcast { - data: NetworkMessage::SumeragiBlock(Box::new(msg)), + data: NetworkMessage::SumeragiBlock(Box::new(msg.into())), }; self.network.broadcast(broadcast); } @@ -116,17 +115,8 @@ impl Sumeragi { self.block_time + self.commit_time } - fn send_events(&self, events: impl IntoIterator>) { - let addr = &self.peer_id.address; - - if self.events_sender.receiver_count() > 0 { - for event in events { - self.events_sender - .send(event.into()) - .map_err(|err| warn!(%addr, ?err, "Event not sent")) - .unwrap_or(0); - } - } + fn send_event(&self, event: impl Into) { + let _ = self.events_sender.send(event.into()); } fn receive_network_packet( @@ -239,13 +229,15 @@ impl Sumeragi { &self.chain_id, &mut state_block, ) + .unpack(|e| self.send_event(e)) .and_then(|block| { block .commit(&self.current_topology) + .unpack(|e| self.send_event(e)) .map_err(|(block, error)| (block.into(), error)) }) { Ok(block) => block, - Err((_, error)) => { + Err(error) => { error!(?error, "Received invalid genesis block"); continue; } @@ -280,12 +272,14 @@ impl Sumeragi { let mut state_block = state.block(); let genesis = BlockBuilder::new(transactions, self.current_topology.clone(), vec![]) .chain(0, &mut state_block) - .sign(&self.key_pair); + .sign(&self.key_pair) + .unpack(|e| self.send_event(e)); - let genesis_msg = BlockCreated::from(genesis.clone()).into(); + let genesis_msg = BlockCreated::from(genesis.clone()); let genesis = genesis .commit(&self.current_topology) + .unpack(|e| self.send_event(e)) .expect("Genesis invalid"); assert!( @@ -319,24 +313,17 @@ impl Sumeragi { info!( addr=%self.peer_id.address, role=%self.current_topology.role(&self.peer_id), - block_height=%state_block.height(), - block_hash=%block.as_ref().hash(), + block=%block.as_ref().header().height, "{}", Strategy::LOG_MESSAGE, ); - state_block - .apply_without_execution(&block) - .expect("Failed to apply block on state. Bailing."); - - let state_events = core::mem::take(&mut state_block.world.events_buffer); - self.send_events(state_events); + let state_events = state_block.apply_without_execution(&block); let new_topology = Topology::recreate_topology( block.as_ref(), 0, state_block.world.peers().cloned().collect(), ); - let events = block.produce_events(); // https://github.com/hyperledger/iroha/issues/3396 // Kura should store the block only upon successful application to the internal state to avoid storing a corrupted block. @@ -346,6 +333,7 @@ impl Sumeragi { // Parameters are updated before updating public copy of sumeragi self.update_params(&state_block); self.cache_transaction(&state_block); + self.current_topology = new_topology; self.connect_peers(&self.current_topology); @@ -353,7 +341,7 @@ impl Sumeragi { state_block.commit(); // NOTE: This sends "Block committed" event, // so it should be done AFTER public facing state update - self.send_events(events); + state_events.into_iter().for_each(|e| self.send_event(e)); } fn update_params(&mut self, state_block: &StateBlock<'_>) { @@ -385,22 +373,23 @@ impl Sumeragi { topology: &Topology, BlockCreated { block }: BlockCreated, ) -> Option> { - let block_hash = block.hash_of_payload(); + let block_height = block.header().height; let addr = &self.peer_id.address; let role = self.current_topology.role(&self.peer_id); - trace!(%addr, %role, block_hash=%block_hash, "Block received, voting..."); + trace!(%addr, %role, block=%block_height, "Block received, voting..."); let mut state_block = state.block(); - let block = match ValidBlock::validate(block, topology, &self.chain_id, &mut state_block) { + let block = match ValidBlock::validate(block, topology, &self.chain_id, &mut state_block) + .unpack(|e| self.send_event(e)) + { Ok(block) => block, - Err((_, error)) => { + Err(error) => { warn!(%addr, %role, ?error, "Block validation failed"); return None; } }; let signed_block = block.sign(&self.key_pair); - Some(VotingBlock::new(signed_block, state_block)) } @@ -433,31 +422,31 @@ impl Sumeragi { #[allow(clippy::suspicious_operation_groupings)] match (message, role) { (BlockMessage::BlockSyncUpdate(BlockSyncUpdate { block }), _) => { - let block_hash = block.hash(); - info!(%addr, %role, hash=%block_hash, "Block sync update received"); + let block_height = block.header().height; + info!(%addr, %role, hash=%block_height, "Block sync update received"); // Release writer before handling block sync let _ = voting_block.take(); - match handle_block_sync(&self.chain_id, block, state) { + match handle_block_sync(&self.chain_id, block, state, &|e| self.send_event(e)) { Ok(BlockSyncOk::CommitBlock(block, state_block)) => { - self.commit_block(block, state_block) + self.commit_block(block, state_block); } Ok(BlockSyncOk::ReplaceTopBlock(block, state_block)) => { warn!( %addr, %role, peer_latest_block_hash=?state_block.latest_block_hash(), peer_latest_block_view_change_index=?state_block.latest_block_view_change_index(), - consensus_latest_block_hash=%block.as_ref().hash(), + consensus_latest_block=%block.as_ref().header().height, consensus_latest_block_view_change_index=%block.as_ref().header().view_change_index, "Soft fork occurred: peer in inconsistent state. Rolling back and replacing top block." ); self.replace_top_block(block, state_block) } Err((_, BlockSyncError::BlockNotValid(error))) => { - error!(%addr, %role, %block_hash, ?error, "Block not valid.") + error!(%addr, %role, block=%block_height, ?error, "Block not valid.") } Err((_, BlockSyncError::SoftForkBlockNotValid(error))) => { - error!(%addr, %role, %block_hash, ?error, "Soft-fork block not valid.") + error!(%addr, %role, %block_height, ?error, "Soft-fork block not valid.") } Err(( _, @@ -470,7 +459,7 @@ impl Sumeragi { %addr, %role, peer_latest_block_hash=?state.view().latest_block_hash(), peer_latest_block_view_change_index=?peer_view_change_index, - consensus_latest_block_hash=%block_hash, + consensus_latest_block=%block_height, consensus_latest_block_view_change_index=%block_view_change_index, "Soft fork doesn't occurred: block has the same or smaller view change index" ); @@ -482,7 +471,7 @@ impl Sumeragi { block_height, }, )) => { - warn!(%addr, %role, %block_hash, %block_height, %peer_height, "Other peer send irrelevant or outdated block to the peer (it's neither `peer_height` nor `peer_height + 1`).") + warn!(%addr, %role, block=%block_height, %peer_height, "Other peer send irrelevant or outdated block to the peer (it's neither `peer_height` nor `peer_height + 1`).") } } } @@ -496,28 +485,30 @@ impl Sumeragi { { error!(%addr, %role, "Received BlockCommitted message, but shouldn't"); } else if let Some(voted_block) = voting_block.take() { - let voting_block_hash = voted_block.block.as_ref().hash_of_payload(); - - if hash == voting_block_hash { - match voted_block - .block - .commit_with_signatures(current_topology, signatures) - { - Ok(committed_block) => { - self.commit_block(committed_block, voted_block.state_block) - } - Err((_, error)) => { - error!(%addr, %role, %hash, ?error, "Block failed to be committed") - } - }; - } else { - error!( - %addr, %role, committed_block_hash=%hash, %voting_block_hash, - "The hash of the committed block does not match the hash of the block stored by the peer." - ); - - *voting_block = Some(voted_block); - }; + match voted_block + .block + .commit_with_signatures(current_topology, signatures, hash) + .unpack(|e| self.send_event(e)) + { + Ok(committed_block) => { + self.commit_block(committed_block, voted_block.state_block) + } + Err(( + valid_block, + BlockValidationError::IncorrectHash { expected, actual }, + )) => { + error!(%addr, %role, %expected, %actual, "The hash of the committed block does not match the hash of the block stored by the peer."); + + *voting_block = Some(VotingBlock { + voted_at: voted_block.voted_at, + block: valid_block, + state_block: voted_block.state_block, + }); + } + Err((_, error)) => { + error!(%addr, %role, %hash, ?error, "Block failed to be committed") + } + } } else { error!(%addr, %role, %hash, "Peer missing voting block") } @@ -531,33 +522,32 @@ impl Sumeragi { let _ = voting_block.take(); if let Some(v_block) = self.vote_for_block(state, ¤t_topology, block_created) { - let block_hash = v_block.block.as_ref().hash_of_payload(); - - let msg = BlockSigned::from(v_block.block.clone()).into(); + let block_height = v_block.block.as_ref().header().height; + let msg = BlockSigned::from(&v_block.block); self.broadcast_packet_to(msg, [current_topology.proxy_tail()]); - info!(%addr, %block_hash, "Block validated, signed and forwarded"); + info!(%addr, block=%block_height, "Block validated, signed and forwarded"); *voting_block = Some(v_block); } } (BlockMessage::BlockCreated(block_created), Role::ObservingPeer) => { let current_topology = current_topology.is_consensus_required().expect( - "Peer has `ObservingPeer` role, which mean that current topology require consensus", - ); + "Peer has `ObservingPeer` role, which mean that current topology require consensus" + ); // Release block writer before creating new one let _ = voting_block.take(); if let Some(v_block) = self.vote_for_block(state, ¤t_topology, block_created) { if current_view_change_index >= 1 { - let block_hash = v_block.block.as_ref().hash(); + let block_height = v_block.block.as_ref().header().height; self.broadcast_packet_to( - BlockSigned::from(v_block.block.clone()).into(), + BlockSigned::from(&v_block.block), [current_topology.proxy_tail()], ); - info!(%addr, %block_hash, "Block validated, signed and forwarded"); + info!(%addr, block=%block_height, "Block validated, signed and forwarded"); *voting_block = Some(v_block); } else { error!(%addr, %role, "Received BlockCreated message, but shouldn't"); @@ -641,33 +631,35 @@ impl Sumeragi { event_recommendations, ) .chain(current_view_change_index, &mut state_block) - .sign(&self.key_pair); + .sign(&self.key_pair) + .unpack(|e| self.send_event(e)); let created_in = create_block_start_time.elapsed(); if let Some(current_topology) = current_topology.is_consensus_required() { - info!(%addr, created_in_ms=%created_in.as_millis(), block_payload_hash=%new_block.as_ref().hash_of_payload(), "Block created"); + info!(%addr, created_in_ms=%created_in.as_millis(), block=%new_block.as_ref().header().height, "Block created"); if created_in > self.pipeline_time() / 2 { warn!("Creating block takes too much time. This might prevent consensus from operating. Consider increasing `commit_time` or decreasing `max_transactions_in_block`"); } *voting_block = Some(VotingBlock::new(new_block.clone(), state_block)); - let msg = BlockCreated::from(new_block).into(); + let msg = BlockCreated::from(new_block); if current_view_change_index >= 1 { self.broadcast_packet(msg); } else { self.broadcast_packet_to(msg, current_topology.voting_peers()); } } else { - match new_block.commit(current_topology) { + match new_block + .commit(current_topology) + .unpack(|e| self.send_event(e)) + { Ok(committed_block) => { - self.broadcast_packet( - BlockCommitted::from(committed_block.clone()).into(), - ); + self.broadcast_packet(BlockCommitted::from(&committed_block)); self.commit_block(committed_block, state_block); } - Err((_, error)) => error!(%addr, role=%Role::Leader, ?error), - } + Err(error) => error!(%addr, role=%Role::Leader, ?error), + }; } } } @@ -677,12 +669,15 @@ impl Sumeragi { let voted_at = voted_block.voted_at; let state_block = voted_block.state_block; - match voted_block.block.commit(current_topology) { + match voted_block + .block + .commit(current_topology) + .unpack(|e| self.send_event(e)) + { Ok(committed_block) => { - info!(voting_block_hash = %committed_block.as_ref().hash(), "Block reached required number of votes"); - - let msg = BlockCommitted::from(committed_block.clone()).into(); + info!(block=%committed_block.as_ref().header().height, "Block reached required number of votes"); + let msg = BlockCommitted::from(&committed_block); let current_topology = current_topology .is_consensus_required() .expect("Peer has `ProxyTail` role, which mean that current topology require consensus"); @@ -863,14 +858,11 @@ pub(crate) fn run( expired }); - let mut expired_transactions = Vec::new(); sumeragi.queue.get_transactions_for_block( &state_view, sumeragi.max_txs_in_block, &mut sumeragi.transaction_cache, - &mut expired_transactions, ); - sumeragi.send_events(expired_transactions.iter().map(expired_event)); let current_view_change_index = sumeragi .prune_view_change_proofs_and_calculate_current_index( @@ -928,7 +920,7 @@ pub(crate) fn run( if node_expects_block { if let Some(VotingBlock { block, .. }) = voting_block.as_ref() { // NOTE: Suspecting the tail node because it hasn't yet committed a block produced by leader - warn!(peer_public_key=%sumeragi.peer_id.public_key, %role, block=%block.as_ref().hash_of_payload(), "Block not committed in due time, requesting view change..."); + warn!(peer_public_key=%sumeragi.peer_id.public_key, %role, block=%block.as_ref().header().height, "Block not committed in due time, requesting view change..."); } else { // NOTE: Suspecting the leader node because it hasn't produced a block // If the current node has a transaction, the leader should have as well @@ -1001,18 +993,6 @@ fn add_signatures( } } -/// Create expired pipeline event for the given transaction. -fn expired_event(txn: &AcceptedTransaction) -> Event { - PipelineEvent { - entity_kind: PipelineEntityKind::Transaction, - status: PipelineStatus::Rejected(PipelineRejectionReason::Transaction( - TransactionRejectionReason::Expired, - )), - hash: txn.as_ref().hash().into(), - } - .into() -} - /// Type enumerating early return types to reduce cyclomatic /// complexity of the main loop items and allow direct short /// circuiting with the `?` operator. Candidate for `impl @@ -1092,10 +1072,11 @@ enum BlockSyncError { }, } -fn handle_block_sync<'state>( +fn handle_block_sync<'state, F: Fn(PipelineEventBox)>( chain_id: &ChainId, block: SignedBlock, state: &'state State, + handle_events: &F, ) -> Result, (SignedBlock, BlockSyncError)> { let block_height = block.header().height; let state_height = state.view().height(); @@ -1111,9 +1092,11 @@ fn handle_block_sync<'state>( Topology::recreate_topology(&last_committed_block, view_change_index, new_peers) }; ValidBlock::validate(block, &topology, chain_id, &mut state_block) + .unpack(handle_events) .and_then(|block| { block .commit(&topology) + .unpack(handle_events) .map_err(|(block, err)| (block.into(), err)) }) .map(|block| BlockSyncOk::CommitBlock(block, state_block)) @@ -1144,9 +1127,11 @@ fn handle_block_sync<'state>( Topology::recreate_topology(&last_committed_block, view_change_index, new_peers) }; ValidBlock::validate(block, &topology, chain_id, &mut state_block) + .unpack(handle_events) .and_then(|block| { block .commit(&topology) + .unpack(handle_events) .map_err(|(block, err)| (block.into(), err)) }) .map_err(|(block, error)| (block, BlockSyncError::SoftForkBlockNotValid(error))) @@ -1214,9 +1199,13 @@ mod tests { // Creating a block of two identical transactions and validating it let block = BlockBuilder::new(vec![tx.clone(), tx], topology.clone(), Vec::new()) .chain(0, &mut state_block) - .sign(leader_key_pair); + .sign(leader_key_pair) + .unpack(|_| {}); - let genesis = block.commit(topology).expect("Block is valid"); + let genesis = block + .commit(topology) + .unpack(|_| {}) + .expect("Block is valid"); state_block.apply(&genesis).expect("Failed to apply block"); state_block.commit(); kura.store_block(genesis); @@ -1256,6 +1245,7 @@ mod tests { BlockBuilder::new(vec![tx1, tx2], topology.clone(), Vec::new()) .chain(0, &mut state_block) .sign(leader_key_pair) + .unpack(|_| {}) }; (state, kura, block.into()) @@ -1276,7 +1266,7 @@ mod tests { // Malform block to make it invalid payload_mut(&mut block).commit_topology.clear(); - let result = handle_block_sync(&chain_id, block, &state); + let result = handle_block_sync(&chain_id, block, &state, &|_| {}); assert!(matches!(result, Err((_, BlockSyncError::BlockNotValid(_))))) } @@ -1292,12 +1282,14 @@ mod tests { let (state, kura, mut block) = create_data_for_test(&chain_id, &topology, &leader_key_pair); let mut state_block = state.block(); - let validated_block = - ValidBlock::validate(block.clone(), &topology, &chain_id, &mut state_block).unwrap(); - let committed_block = validated_block.commit(&topology).expect("Block is valid"); - state_block - .apply_without_execution(&committed_block) - .expect("Failed to apply block"); + let committed_block = + ValidBlock::validate(block.clone(), &topology, &chain_id, &mut state_block) + .unpack(|_| {}) + .unwrap() + .commit(&topology) + .unpack(|_| {}) + .expect("Block is valid"); + let _wsv_events = state_block.apply_without_execution(&committed_block); state_block.commit(); kura.store_block(committed_block); @@ -1305,7 +1297,7 @@ mod tests { payload_mut(&mut block).commit_topology.clear(); payload_mut(&mut block).header.view_change_index = 1; - let result = handle_block_sync(&chain_id, block, &state); + let result = handle_block_sync(&chain_id, block, &state, &|_| {}); assert!(matches!( result, Err((_, BlockSyncError::SoftForkBlockNotValid(_))) @@ -1324,7 +1316,7 @@ mod tests { // Change block height payload_mut(&mut block).header.height = 42; - let result = handle_block_sync(&chain_id, block, &state); + let result = handle_block_sync(&chain_id, block, &state, &|_| {}); assert!(matches!( result, Err(( @@ -1348,7 +1340,7 @@ mod tests { leader_key_pair.public_key().clone(), )]); let (state, _, block) = create_data_for_test(&chain_id, &topology, &leader_key_pair); - let result = handle_block_sync(&chain_id, block, &state); + let result = handle_block_sync(&chain_id, block, &state, &|_| {}); assert!(matches!(result, Ok(BlockSyncOk::CommitBlock(_, _)))) } @@ -1364,12 +1356,14 @@ mod tests { let (state, kura, mut block) = create_data_for_test(&chain_id, &topology, &leader_key_pair); let mut state_block = state.block(); - let validated_block = - ValidBlock::validate(block.clone(), &topology, &chain_id, &mut state_block).unwrap(); - let committed_block = validated_block.commit(&topology).expect("Block is valid"); - state_block - .apply_without_execution(&committed_block) - .expect("Failed to apply block"); + let committed_block = + ValidBlock::validate(block.clone(), &topology, &chain_id, &mut state_block) + .unpack(|_| {}) + .unwrap() + .commit(&topology) + .unpack(|_| {}) + .expect("Block is valid"); + let _wsv_events = state_block.apply_without_execution(&committed_block); state_block.commit(); kura.store_block(committed_block); @@ -1378,7 +1372,7 @@ mod tests { // Increase block view change index payload_mut(&mut block).header.view_change_index = 42; - let result = handle_block_sync(&chain_id, block, &state); + let result = handle_block_sync(&chain_id, block, &state, &|_| {}); assert!(matches!(result, Ok(BlockSyncOk::ReplaceTopBlock(_, _)))) } @@ -1397,12 +1391,14 @@ mod tests { payload_mut(&mut block).header.view_change_index = 42; let mut state_block = state.block(); - let validated_block = - ValidBlock::validate(block.clone(), &topology, &chain_id, &mut state_block).unwrap(); - let committed_block = validated_block.commit(&topology).expect("Block is valid"); - state_block - .apply_without_execution(&committed_block) - .expect("Failed to apply block"); + let committed_block = + ValidBlock::validate(block.clone(), &topology, &chain_id, &mut state_block) + .unpack(|_| {}) + .unwrap() + .commit(&topology) + .unpack(|_| {}) + .expect("Block is valid"); + let _wsv_events = state_block.apply_without_execution(&committed_block); state_block.commit(); kura.store_block(committed_block); assert_eq!(state.view().latest_block_view_change_index(), 42); @@ -1410,7 +1406,7 @@ mod tests { // Decrease block view change index back payload_mut(&mut block).header.view_change_index = 0; - let result = handle_block_sync(&chain_id, block, &state); + let result = handle_block_sync(&chain_id, block, &state, &|_| {}); assert!(matches!( result, Err(( @@ -1437,7 +1433,7 @@ mod tests { payload_mut(&mut block).header.view_change_index = 42; payload_mut(&mut block).header.height = 1; - let result = handle_block_sync(&chain_id, block, &state); + let result = handle_block_sync(&chain_id, block, &state, &|_| {}); assert!(matches!( result, Err(( diff --git a/core/src/sumeragi/message.rs b/core/src/sumeragi/message.rs index b0a80207072..c5d4fa27fa7 100644 --- a/core/src/sumeragi/message.rs +++ b/core/src/sumeragi/message.rs @@ -62,14 +62,14 @@ pub struct BlockSigned { pub signatures: SignaturesOf, } -impl From for BlockSigned { - fn from(block: ValidBlock) -> Self { +impl From<&ValidBlock> for BlockSigned { + fn from(block: &ValidBlock) -> Self { let block_hash = block.as_ref().hash_of_payload(); - let SignedBlock::V1(block) = block.into(); + let block_signatures = block.as_ref().signatures().clone(); Self { hash: block_hash, - signatures: block.signatures, + signatures: block_signatures, } } } @@ -79,14 +79,14 @@ impl From for BlockSigned { #[non_exhaustive] pub struct BlockCommitted { /// Hash of the block being signed. - pub hash: HashOf, + pub hash: HashOf, /// Set of signatures. pub signatures: SignaturesOf, } -impl From for BlockCommitted { - fn from(block: CommittedBlock) -> Self { - let block_hash = block.as_ref().hash_of_payload(); +impl From<&CommittedBlock> for BlockCommitted { + fn from(block: &CommittedBlock) -> Self { + let block_hash = block.as_ref().hash(); let block_signatures = block.as_ref().signatures().clone(); Self { diff --git a/core/src/sumeragi/mod.rs b/core/src/sumeragi/mod.rs index 1e10895b992..f59a7ee6259 100644 --- a/core/src/sumeragi/mod.rs +++ b/core/src/sumeragi/mod.rs @@ -4,7 +4,7 @@ use std::{ fmt::{self, Debug, Formatter}, sync::{mpsc, Arc}, - time::{Duration, Instant}, + time::{Duration, Instant, SystemTime}, }; use eyre::{Result, WrapErr as _}; @@ -129,9 +129,13 @@ impl SumeragiHandle { #[allow(clippy::cast_possible_truncation)] if let Some(timestamp) = state_view.genesis_timestamp() { + let curr_time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Failed to get the current system time"); + // this will overflow in 584942417years. self.metrics.uptime_since_genesis_ms.set( - (current_time() - timestamp) + (curr_time - timestamp) .as_millis() .try_into() .expect("Timestamp should fit into u64"), @@ -193,24 +197,33 @@ impl SumeragiHandle { chain_id: &ChainId, block: &SignedBlock, state_block: &mut StateBlock<'_>, + events_sender: &EventsSender, mut current_topology: Topology, ) -> Topology { // NOTE: topology need to be updated up to block's view_change_index current_topology.rotate_all_n(block.header().view_change_index); let block = ValidBlock::validate(block.clone(), ¤t_topology, chain_id, state_block) - .expect("Kura blocks should be valid") + .unpack(|e| { + let _ = events_sender.send(e.into()); + }) + .expect("Kura: Invalid block") .commit(¤t_topology) - .expect("Kura blocks should be valid"); + .unpack(|e| { + let _ = events_sender.send(e.into()); + }) + .expect("Kura: Invalid block"); if block.as_ref().header().is_genesis() { *state_block.world.trusted_peers_ids = block.as_ref().commit_topology().clone(); } - state_block.apply_without_execution(&block).expect( - "Block application in init should not fail. \ - Blocks loaded from kura assumed to be valid", - ); + state_block + .apply_without_execution(&block) + .into_iter() + .for_each(|e| { + let _ = events_sender.send(e); + }); Topology::recreate_topology( block.as_ref(), @@ -278,6 +291,7 @@ impl SumeragiHandle { &common_config.chain_id, &block, &mut state_block, + &events_sender, current_topology, ); state_block.commit(); @@ -356,16 +370,21 @@ pub const PEERS_CONNECT_INTERVAL: Duration = Duration::from_secs(1); pub const TELEMETRY_INTERVAL: Duration = Duration::from_secs(5); /// Structure represents a block that is currently in discussion. -#[non_exhaustive] pub struct VotingBlock<'state> { + /// Valid Block + block: ValidBlock, /// At what time has this peer voted for this block pub voted_at: Instant, - /// Valid Block - pub block: ValidBlock, /// [`WorldState`] after applying transactions to it but before it was committed pub state_block: StateBlock<'state>, } +impl AsRef for VotingBlock<'_> { + fn as_ref(&self) -> &ValidBlock { + &self.block + } +} + impl VotingBlock<'_> { /// Construct new `VotingBlock` with current time. pub fn new(block: ValidBlock, state_block: StateBlock<'_>) -> VotingBlock { @@ -382,8 +401,8 @@ impl VotingBlock<'_> { voted_at: Instant, ) -> VotingBlock { VotingBlock { - voted_at, block, + voted_at, state_block, } } diff --git a/core/src/tx.rs b/core/src/tx.rs index 26e0858a4e5..1ae6b4b0b2b 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}, + transaction::{error::TransactionLimitError, TransactionLimits, TransactionPayload}, }; use iroha_genesis::GenesisTransaction; use iroha_logger::{debug, error}; diff --git a/core/test_network/Cargo.toml b/core/test_network/Cargo.toml index 22cbae6888a..183eb00e4d5 100644 --- a/core/test_network/Cargo.toml +++ b/core/test_network/Cargo.toml @@ -8,7 +8,7 @@ authors.workspace = true license.workspace = true [dependencies] -iroha = { workspace = true, features = ["test-network"] } +iroha = { workspace = true, features = ["test_network"] } iroha_crypto = { workspace = true } iroha_client = { workspace = true } iroha_core = { workspace = true } diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index 012f475eda0..df2843afa06 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -14,7 +14,7 @@ use iroha_client::{ }; use iroha_config::parameters::actual::Root as Config; pub use iroha_core::state::StateReadOnly; -use iroha_crypto::prelude::*; +use iroha_crypto::KeyPair; use iroha_data_model::{query::QueryOutputBox, ChainId}; use iroha_genesis::{GenesisNetwork, RawGenesisBlockFile}; use iroha_logger::InstrumentFutures; @@ -54,11 +54,11 @@ pub fn get_chain_id() -> ChainId { /// Get a standardised key-pair from the hard-coded literals. pub fn get_key_pair() -> KeyPair { KeyPair::new( - PublicKey::from_str( + iroha_crypto::PublicKey::from_str( "ed01207233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0", ).unwrap(), - PrivateKey::from_hex( - Algorithm::Ed25519, + iroha_crypto::PrivateKey::from_hex( + iroha_crypto::Algorithm::Ed25519, "9AC47ABF59B356E0BD7DCBBBB4DEC080E302156A48CA907E47CB6AEA1D32719E7233BFC89DCBD68C19FDE6CE6158225298EC1131B6A130D1AEB454C1AB5183C0" ).unwrap() ).unwrap() @@ -689,7 +689,7 @@ pub trait TestClient: Sized { fn test_with_account(api_url: &SocketAddr, keys: KeyPair, account_id: &AccountId) -> Self; /// Loop for events with filter and handler function - fn for_each_event(self, event_filter: impl Into, f: impl Fn(Result)); + fn for_each_event(self, event_filter: impl Into, f: impl Fn(Result)); /// Submit instruction with polling /// @@ -828,9 +828,9 @@ impl TestClient for Client { Client::new(config) } - fn for_each_event(self, event_filter: impl Into, f: impl Fn(Result)) { + fn for_each_event(self, event_filter: impl Into, f: impl Fn(Result)) { for event_result in self - .listen_for_events(event_filter) + .listen_for_events([event_filter]) .expect("Failed to create event iterator.") { f(event_result) diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index f1662780479..aafd7868459 100755 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -27,8 +27,6 @@ use alloc::{ }; use core::{borrow::Borrow, fmt, str::FromStr}; -#[cfg(feature = "base64")] -pub use base64; #[cfg(not(feature = "ffi_import"))] pub use blake2; use derive_more::Display; @@ -857,11 +855,6 @@ mod ffi { pub(crate) use ffi_item; } -/// The prelude re-exports most commonly used items from this crate. -pub mod prelude { - pub use super::{Algorithm, Hash, KeyPair, PrivateKey, PublicKey, Signature}; -} - #[cfg(test)] mod tests { use parity_scale_codec::{Decode, Encode}; diff --git a/data_model/derive/src/enum_ref.rs b/data_model/derive/src/enum_ref.rs index 8215be75795..eefb58fab78 100644 --- a/data_model/derive/src/enum_ref.rs +++ b/data_model/derive/src/enum_ref.rs @@ -151,7 +151,7 @@ impl ToTokens for EnumRef { quote! { #attrs - pub(crate) enum #ident<'a> #impl_generics #where_clause { + pub(super) enum #ident<'a> #impl_generics #where_clause { #(#variants),* } } diff --git a/data_model/derive/src/lib.rs b/data_model/derive/src/lib.rs index 384ca813542..32b11ecee39 100644 --- a/data_model/derive/src/lib.rs +++ b/data_model/derive/src/lib.rs @@ -15,35 +15,39 @@ use proc_macro2::TokenStream; /// # Example /// /// ``` -/// use iroha_data_model_derive::EnumRef; -/// use parity_scale_codec::Encode; -/// -/// #[derive(EnumRef)] -/// #[enum_ref(derive(Encode))] -/// pub enum InnerEnum { -/// A(u32), -/// B(i32) -/// } +/// mod model { +/// use iroha_data_model_derive::EnumRef; +/// use parity_scale_codec::Encode; +/// +/// #[derive(EnumRef)] +/// #[enum_ref(derive(Encode))] +/// pub enum InnerEnum { +/// A(u32), +/// B(i32) +/// } /// -/// #[derive(EnumRef)] -/// #[enum_ref(derive(Encode))] -/// pub enum OuterEnum { -/// A(String), -/// #[enum_ref(transparent)] -/// B(InnerEnum), +/// #[derive(EnumRef)] +/// #[enum_ref(derive(Encode))] +/// pub enum OuterEnum { +/// A(String), +/// #[enum_ref(transparent)] +/// B(InnerEnum), +/// } /// } /// /// /* will produce: -/// #[derive(Encode)] -/// pub(crate) enum InnerEnumRef<'a> { -/// A(&'a u32), -/// B(&'a i32), -/// } +/// mod model { +/// #[derive(Encode)] +/// pub(super) enum InnerEnumRef<'a> { +/// A(&'a u32), +/// B(&'a i32), +/// } /// -/// #[derive(Encode)] -/// pub(crate) enum OuterEnumRef<'a> { -/// A(&'a String), -/// B(InnerEnumRef<'a>), +/// #[derive(Encode)] +/// pub(super) enum OuterEnumRef<'a> { +/// A(&'a String), +/// B(InnerEnumRef<'a>), +/// } /// } /// */ /// ``` diff --git a/data_model/derive/src/model.rs b/data_model/derive/src/model.rs index a5fdb7a7510..0547fc99ab7 100644 --- a/data_model/derive/src/model.rs +++ b/data_model/derive/src/model.rs @@ -7,7 +7,6 @@ use syn::{parse_quote, Attribute}; pub fn impl_model(emitter: &mut Emitter, input: &syn::ItemMod) -> TokenStream { let syn::ItemMod { attrs, - vis, mod_token, ident, content, @@ -15,14 +14,6 @@ pub fn impl_model(emitter: &mut Emitter, input: &syn::ItemMod) -> TokenStream { .. } = input; - let syn::Visibility::Public(vis_public) = vis else { - emit!( - emitter, - input, - "The `model` attribute can only be used on public modules" - ); - return quote!(); - }; if ident != "model" { emit!( emitter, @@ -38,7 +29,7 @@ pub fn impl_model(emitter: &mut Emitter, input: &syn::ItemMod) -> TokenStream { quote! { #(#attrs)* #[allow(missing_docs)] - #vis_public #mod_token #ident { + #mod_token #ident { #(#items_code)* }#semi } diff --git a/data_model/src/account.rs b/data_model/src/account.rs index 2383bdc21ac..ce3f9582770 100644 --- a/data_model/src/account.rs +++ b/data_model/src/account.rs @@ -431,6 +431,8 @@ pub mod prelude { #[cfg(test)] mod tests { + #[cfg(not(feature = "std"))] + use alloc::{vec, vec::Vec}; use core::cmp::Ordering; use iroha_crypto::{KeyPair, PublicKey}; diff --git a/data_model/src/block.rs b/data_model/src/block.rs index 93ce5bec045..e9d3c102074 100644 --- a/data_model/src/block.rs +++ b/data_model/src/block.rs @@ -9,7 +9,6 @@ use alloc::{boxed::Box, format, string::String, vec::Vec}; use core::{fmt::Display, time::Duration}; use derive_more::Display; -use getset::Getters; #[cfg(all(feature = "std", feature = "transparent_api"))] use iroha_crypto::KeyPair; use iroha_crypto::{HashOf, MerkleTree, SignaturesOf}; @@ -26,6 +25,8 @@ use crate::{events::prelude::*, peer, transaction::prelude::*}; #[model] pub mod model { + use getset::{CopyGetters, Getters}; + use super::*; #[derive( @@ -37,6 +38,7 @@ pub mod model { PartialOrd, Ord, Getters, + CopyGetters, Decode, Encode, Deserialize, @@ -48,24 +50,24 @@ pub mod model { display(fmt = "Block №{height} (hash: {});", "HashOf::new(&self)") )] #[cfg_attr(not(feature = "std"), display(fmt = "Block №{height}"))] - #[getset(get = "pub")] #[allow(missing_docs)] #[ffi_type] pub struct BlockHeader { /// Number of blocks in the chain including this block. + #[getset(get_copy = "pub")] pub height: u64, /// Creation timestamp (unix time in milliseconds). #[getset(skip)] - pub timestamp_ms: u64, + pub creation_time_ms: u64, /// Hash of the previous block in the chain. - pub previous_block_hash: Option>, + #[getset(get = "pub")] + pub prev_block_hash: Option>, /// Hash of merkle tree root of transactions' hashes. - pub transactions_hash: Option>>, + #[getset(get = "pub")] + pub transactions_hash: HashOf>, /// Value of view change index. Used to resolve soft forks. - pub view_change_index: u64, #[getset(skip)] - /// Estimation of consensus duration (in milliseconds). - pub consensus_estimation_ms: u64, + pub view_change_index: u64, } #[derive( @@ -76,7 +78,6 @@ pub mod model { Eq, PartialOrd, Ord, - Getters, Decode, Encode, Deserialize, @@ -84,45 +85,28 @@ pub mod model { IntoSchema, )] #[display(fmt = "({header})")] - #[getset(get = "pub")] #[allow(missing_docs)] - #[ffi_type] - pub struct BlockPayload { + pub(crate) struct BlockPayload { /// Block header pub header: BlockHeader, /// Topology of the network at the time of block commit. - #[getset(skip)] // FIXME: Because ffi related issues pub commit_topology: UniqueVec, /// array of transactions, which successfully passed validation and consensus step. - #[getset(skip)] // FIXME: Because ffi related issues pub transactions: Vec, /// Event recommendations. - #[getset(skip)] // NOTE: Unused ATM - pub event_recommendations: Vec, + pub event_recommendations: Vec, } /// Signed block #[version_with_scale(version = 1, versioned_alias = "SignedBlock")] #[derive( - Debug, - Display, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Getters, - Encode, - Serialize, - IntoSchema, + Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord, Encode, Serialize, IntoSchema, )] #[cfg_attr(not(feature = "std"), display(fmt = "Signed block"))] #[cfg_attr(feature = "std", display(fmt = "{}", "self.hash()"))] - #[getset(get = "pub")] #[ffi_type] pub struct SignedBlockV1 { /// Signatures of peers which approved this block. - #[getset(skip)] pub signatures: SignaturesOf, /// Block payload pub payload: BlockPayload, @@ -134,13 +118,6 @@ declare_versioned!(SignedBlock 1..2, Debug, Clone, PartialEq, Eq, PartialOrd, Or #[cfg(all(not(feature = "ffi_export"), not(feature = "ffi_import")))] declare_versioned!(SignedBlock 1..2, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, FromVariant, IntoSchema); -impl BlockPayload { - /// Calculate block payload [`Hash`](`iroha_crypto::HashOf`). - pub fn hash(&self) -> iroha_crypto::HashOf { - iroha_crypto::HashOf::new(self) - } -} - impl BlockHeader { /// Checks if it's a header of a genesis block. #[inline] @@ -150,13 +127,8 @@ impl BlockHeader { } /// Creation timestamp - pub fn timestamp(&self) -> Duration { - Duration::from_millis(self.timestamp_ms) - } - - /// Consensus estimation - pub fn consensus_estimation(&self) -> Duration { - Duration::from_millis(self.consensus_estimation_ms) + pub fn creation_time(&self) -> Duration { + Duration::from_millis(self.creation_time_ms) } } @@ -168,21 +140,21 @@ impl SignedBlockV1 { } impl SignedBlock { - /// Block transactions + /// Block header #[inline] - pub fn transactions(&self) -> impl ExactSizeIterator { + pub fn header(&self) -> &BlockHeader { let SignedBlock::V1(block) = self; - block.payload.transactions.iter() + &block.payload.header } - /// Block header + /// Block transactions #[inline] - pub fn header(&self) -> &BlockHeader { + pub fn transactions(&self) -> impl ExactSizeIterator { let SignedBlock::V1(block) = self; - block.payload.header() + block.payload.transactions.iter() } - /// Block commit topology + /// Topology of the network at the time of block commit. #[inline] #[cfg(feature = "transparent_api")] pub fn commit_topology(&self) -> &UniqueVec { @@ -202,18 +174,19 @@ impl SignedBlock { pub fn hash(&self) -> HashOf { iroha_crypto::HashOf::new(self) } +} +#[cfg(feature = "transparent_api")] +impl SignedBlock { /// Calculate block payload [`Hash`](`iroha_crypto::HashOf`). #[inline] #[cfg(feature = "std")] - #[cfg(feature = "transparent_api")] pub fn hash_of_payload(&self) -> iroha_crypto::HashOf { let SignedBlock::V1(block) = self; iroha_crypto::HashOf::new(&block.payload) } /// Add additional signatures to this block - #[cfg(feature = "transparent_api")] #[must_use] pub fn sign(mut self, key_pair: &KeyPair) -> Self { let SignedBlock::V1(block) = &mut self; @@ -227,7 +200,6 @@ impl SignedBlock { /// # Errors /// /// If given signature doesn't match block hash - #[cfg(feature = "transparent_api")] pub fn add_signature( &mut self, signature: iroha_crypto::SignatureOf, @@ -242,7 +214,6 @@ impl SignedBlock { } /// Add additional signatures to this block - #[cfg(feature = "transparent_api")] pub fn replace_signatures( &mut self, signatures: iroha_crypto::SignaturesOf, @@ -292,7 +263,7 @@ mod candidate { } fn validate_header(&self) -> Result<(), &'static str> { - let actual_txs_hash = self.payload.header().transactions_hash; + let actual_txs_hash = self.payload.header.transactions_hash; let expected_txs_hash = self .payload @@ -300,7 +271,8 @@ mod candidate { .iter() .map(|value| value.as_ref().hash()) .collect::>() - .hash(); + .hash() + .unwrap(); if expected_txs_hash != actual_txs_hash { return Err("Transactions' hash incorrect. Expected: {expected_txs_hash:?}, actual: {actual_txs_hash:?}"); diff --git a/data_model/src/events/data/filters.rs b/data_model/src/events/data/filters.rs index 4edc08c828e..92743725aaf 100644 --- a/data_model/src/events/data/filters.rs +++ b/data_model/src/events/data/filters.rs @@ -705,7 +705,6 @@ impl EventFilter for DataEventFilter { (DataEvent::Peer(event), Peer(filter)) => filter.matches(event), (DataEvent::Trigger(event), Trigger(filter)) => filter.matches(event), (DataEvent::Role(event), Role(filter)) => filter.matches(event), - (DataEvent::PermissionToken(_), PermissionTokenSchemaUpdate) => true, (DataEvent::Configuration(event), Configuration(filter)) => filter.matches(event), (DataEvent::Executor(event), Executor(filter)) => filter.matches(event), diff --git a/data_model/src/events/mod.rs b/data_model/src/events/mod.rs index 94c003526bc..d9e59fd6a8c 100644 --- a/data_model/src/events/mod.rs +++ b/data_model/src/events/mod.rs @@ -7,9 +7,11 @@ use iroha_data_model_derive::model; use iroha_macro::FromVariant; use iroha_schema::IntoSchema; use parity_scale_codec::{Decode, Encode}; +use pipeline::{BlockEvent, TransactionEvent}; use serde::{Deserialize, Serialize}; pub use self::model::*; +use self::pipeline::{BlockEventFilter, TransactionEventFilter}; pub mod data; pub mod execute_trigger; @@ -37,9 +39,9 @@ pub mod model { IntoSchema, )] #[ffi_type] - pub enum Event { + pub enum EventBox { /// Pipeline event. - Pipeline(pipeline::PipelineEvent), + Pipeline(pipeline::PipelineEventBox), /// Data event. Data(data::DataEvent), /// Time event. @@ -85,7 +87,7 @@ pub mod model { #[ffi_type(opaque)] pub enum EventFilterBox { /// Listen to pipeline events with filter. - Pipeline(pipeline::PipelineEventFilter), + Pipeline(pipeline::PipelineEventFilterBox), /// Listen to data events with filter. Data(data::DataEventFilter), /// Listen to time events with filter. @@ -116,7 +118,7 @@ pub mod model { #[ffi_type(opaque)] pub enum TriggeringEventFilterBox { /// Listen to pipeline events with filter. - Pipeline(pipeline::PipelineEventFilter), + Pipeline(pipeline::PipelineEventFilterBox), /// Listen to data events with filter. Data(data::DataEventFilter), /// Listen to time events with filter. @@ -126,6 +128,62 @@ pub mod model { } } +impl From for EventBox { + fn from(source: TransactionEvent) -> Self { + Self::Pipeline(source.into()) + } +} + +impl From for EventBox { + fn from(source: BlockEvent) -> Self { + Self::Pipeline(source.into()) + } +} + +impl From for EventFilterBox { + fn from(source: TransactionEventFilter) -> Self { + Self::Pipeline(source.into()) + } +} + +impl From for EventFilterBox { + fn from(source: BlockEventFilter) -> Self { + Self::Pipeline(source.into()) + } +} + +impl TryFrom for TransactionEvent { + type Error = iroha_macro::error::ErrorTryFromEnum; + + fn try_from(event: EventBox) -> Result { + use iroha_macro::error::ErrorTryFromEnum; + + let EventBox::Pipeline(pipeline_event) = event else { + return Err(ErrorTryFromEnum::default()); + }; + + pipeline_event + .try_into() + .map_err(|_| ErrorTryFromEnum::default()) + } +} + +impl TryFrom for BlockEvent { + type Error = iroha_macro::error::ErrorTryFromEnum; + + fn try_from(event: EventBox) -> Result { + use iroha_macro::error::ErrorTryFromEnum; + + let EventBox::Pipeline(pipeline_event) = event else { + return Err(ErrorTryFromEnum::default()); + }; + + pipeline_event + .try_into() + .map_err(|_| ErrorTryFromEnum::default()) + } +} + /// Trait for filters #[cfg(feature = "transparent_api")] pub trait EventFilter { @@ -156,25 +214,27 @@ pub trait EventFilter { #[cfg(feature = "transparent_api")] impl EventFilter for EventFilterBox { - type Event = Event; + type Event = EventBox; /// Apply filter to event. - fn matches(&self, event: &Event) -> bool { + fn matches(&self, event: &EventBox) -> bool { match (event, self) { - (Event::Pipeline(event), Self::Pipeline(filter)) => filter.matches(event), - (Event::Data(event), Self::Data(filter)) => filter.matches(event), - (Event::Time(event), Self::Time(filter)) => filter.matches(event), - (Event::ExecuteTrigger(event), Self::ExecuteTrigger(filter)) => filter.matches(event), - (Event::TriggerCompleted(event), Self::TriggerCompleted(filter)) => { + (EventBox::Pipeline(event), Self::Pipeline(filter)) => filter.matches(event), + (EventBox::Data(event), Self::Data(filter)) => filter.matches(event), + (EventBox::Time(event), Self::Time(filter)) => filter.matches(event), + (EventBox::ExecuteTrigger(event), Self::ExecuteTrigger(filter)) => { + filter.matches(event) + } + (EventBox::TriggerCompleted(event), Self::TriggerCompleted(filter)) => { filter.matches(event) } // Fail to compile in case when new variant to event or filter is added ( - Event::Pipeline(_) - | Event::Data(_) - | Event::Time(_) - | Event::ExecuteTrigger(_) - | Event::TriggerCompleted(_), + EventBox::Pipeline(_) + | EventBox::Data(_) + | EventBox::Time(_) + | EventBox::ExecuteTrigger(_) + | EventBox::TriggerCompleted(_), Self::Pipeline(_) | Self::Data(_) | Self::Time(_) @@ -187,22 +247,24 @@ impl EventFilter for EventFilterBox { #[cfg(feature = "transparent_api")] impl EventFilter for TriggeringEventFilterBox { - type Event = Event; + type Event = EventBox; /// Apply filter to event. - fn matches(&self, event: &Event) -> bool { + fn matches(&self, event: &EventBox) -> bool { match (event, self) { - (Event::Pipeline(event), Self::Pipeline(filter)) => filter.matches(event), - (Event::Data(event), Self::Data(filter)) => filter.matches(event), - (Event::Time(event), Self::Time(filter)) => filter.matches(event), - (Event::ExecuteTrigger(event), Self::ExecuteTrigger(filter)) => filter.matches(event), + (EventBox::Pipeline(event), Self::Pipeline(filter)) => filter.matches(event), + (EventBox::Data(event), Self::Data(filter)) => filter.matches(event), + (EventBox::Time(event), Self::Time(filter)) => filter.matches(event), + (EventBox::ExecuteTrigger(event), Self::ExecuteTrigger(filter)) => { + filter.matches(event) + } // Fail to compile in case when new variant to event or filter is added ( - Event::Pipeline(_) - | Event::Data(_) - | Event::Time(_) - | Event::ExecuteTrigger(_) - | Event::TriggerCompleted(_), + EventBox::Pipeline(_) + | EventBox::Data(_) + | EventBox::Time(_) + | EventBox::ExecuteTrigger(_) + | EventBox::TriggerCompleted(_), Self::Pipeline(_) | Self::Data(_) | Self::Time(_) | Self::ExecuteTrigger(_), ) => false, } @@ -279,16 +341,16 @@ pub mod stream { /// Event sent by the peer. #[derive(Debug, Clone, Decode, Encode, IntoSchema)] #[repr(transparent)] - pub struct EventMessage(pub Event); + pub struct EventMessage(pub EventBox); /// Message sent by the stream consumer. /// Request sent by the client to subscribe to events. #[derive(Debug, Clone, Constructor, Decode, Encode, IntoSchema)] #[repr(transparent)] - pub struct EventSubscriptionRequest(pub EventFilterBox); + pub struct EventSubscriptionRequest(pub Vec); } - impl From for Event { + impl From for EventBox { fn from(source: EventMessage) -> Self { source.0 } @@ -303,7 +365,7 @@ pub mod prelude { pub use super::EventFilter; pub use super::{ data::prelude::*, execute_trigger::prelude::*, pipeline::prelude::*, time::prelude::*, - trigger_completed::prelude::*, Event, EventFilterBox, TriggeringEventFilterBox, + trigger_completed::prelude::*, EventBox, EventFilterBox, TriggeringEventFilterBox, TriggeringEventType, }; } diff --git a/data_model/src/events/pipeline.rs b/data_model/src/events/pipeline.rs index 5d3b962144f..7f1079d1120 100644 --- a/data_model/src/events/pipeline.rs +++ b/data_model/src/events/pipeline.rs @@ -1,59 +1,55 @@ //! Pipeline events. #[cfg(not(feature = "std"))] -use alloc::{format, string::String, vec::Vec}; +use alloc::{boxed::Box, format, string::String, vec::Vec}; -use getset::Getters; -use iroha_crypto::Hash; +use iroha_crypto::HashOf; use iroha_data_model_derive::model; use iroha_macro::FromVariant; use iroha_schema::IntoSchema; use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; -use strum::EnumDiscriminants; pub use self::model::*; +use crate::{ + block::{BlockHeader, SignedBlock}, + transaction::SignedTransaction, +}; #[model] pub mod model { + use getset::Getters; + use super::*; - /// [`Event`] filter. #[derive( Debug, Clone, - Copy, PartialEq, Eq, PartialOrd, Ord, - Default, - Getters, + FromVariant, Decode, Encode, - Serialize, Deserialize, + Serialize, IntoSchema, )] - pub struct PipelineEventFilter { - /// If `Some::`, filter by the [`EntityKind`]. If `None`, accept all the [`EntityKind`]. - pub(super) entity_kind: Option, - /// If `Some::`, filter by the [`StatusKind`]. If `None`, accept all the [`StatusKind`]. - pub(super) status_kind: Option, - /// If `Some::`, filter by the [`struct@Hash`]. If `None`, accept all the [`struct@Hash`]. - // TODO: Can we make hash typed like HashOf? - pub(super) hash: Option, + #[ffi_type(opaque)] + pub enum PipelineEventBox { + Transaction(TransactionEvent), + Block(BlockEvent), } - /// The kind of the pipeline entity. #[derive( Debug, Clone, - Copy, PartialEq, Eq, PartialOrd, Ord, + Getters, Decode, Encode, Deserialize, @@ -61,15 +57,13 @@ pub mod model { IntoSchema, )] #[ffi_type] - #[repr(u8)] - pub enum PipelineEntityKind { - /// Block - Block, - /// Transaction - Transaction, + #[getset(get = "pub")] + pub struct BlockEvent { + pub header: BlockHeader, + pub hash: HashOf, + pub status: BlockStatus, } - /// Strongly-typed [`Event`] that tells the receiver the kind and the hash of the changed entity as well as its [`Status`]. #[derive( Debug, Clone, @@ -84,18 +78,15 @@ pub mod model { Serialize, IntoSchema, )] - #[getset(get = "pub")] #[ffi_type] - pub struct PipelineEvent { - /// [`EntityKind`] of the entity that caused this [`Event`]. - pub entity_kind: PipelineEntityKind, - /// [`Status`] of the entity that caused this [`Event`]. - pub status: PipelineStatus, - /// [`struct@Hash`] of the entity that caused this [`Event`]. - pub hash: Hash, + #[getset(get = "pub")] + pub struct TransactionEvent { + pub hash: HashOf, + pub block_height: Option, + pub status: TransactionStatus, } - /// [`Status`] of the entity. + /// Report of block's status in the pipeline #[derive( Debug, Clone, @@ -103,129 +94,221 @@ pub mod model { Eq, PartialOrd, Ord, - FromVariant, - EnumDiscriminants, Decode, Encode, + Deserialize, Serialize, + IntoSchema, + )] + #[ffi_type(opaque)] + pub enum BlockStatus { + /// Block was approved to participate in consensus + Approved, + /// Block was rejected by consensus + Rejected(crate::block::error::BlockRejectionReason), + /// Block has passed consensus successfully + Committed, + /// Changes have been reflected in the WSV + Applied, + } + + /// Report of transaction's status in the pipeline + #[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Decode, + Encode, Deserialize, + Serialize, IntoSchema, )] - #[strum_discriminants( - name(PipelineStatusKind), - derive(PartialOrd, Ord, Decode, Encode, Deserialize, Serialize, IntoSchema,) + #[ffi_type(opaque)] + pub enum TransactionStatus { + /// Transaction was received and enqueued + Queued, + /// Transaction was dropped(not stored in a block) + Expired, + /// Transaction was stored in the block as valid + Approved, + /// Transaction was stored in the block as invalid + Rejected(Box), + } + + #[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + FromVariant, + Decode, + Encode, + Deserialize, + Serialize, + IntoSchema, )] #[ffi_type] - pub enum PipelineStatus { - /// Entity has been seen in the blockchain but has not passed validation. - Validating, - /// Entity was rejected during validation. - Rejected(PipelineRejectionReason), - /// Entity has passed validation. - Committed, + pub enum PipelineEventFilterBox { + Transaction(TransactionEventFilter), + Block(BlockEventFilter), } - /// The reason for rejecting pipeline entity such as transaction or block. #[derive( Debug, - displaydoc::Display, Clone, PartialEq, Eq, PartialOrd, Ord, - FromVariant, + Default, + Getters, Decode, Encode, Deserialize, Serialize, IntoSchema, )] - #[cfg_attr(feature = "std", derive(thiserror::Error))] #[ffi_type] - pub enum PipelineRejectionReason { - /// Block was rejected - Block(#[cfg_attr(feature = "std", source)] crate::block::error::BlockRejectionReason), - /// Transaction was rejected - Transaction( - #[cfg_attr(feature = "std", source)] - crate::transaction::error::TransactionRejectionReason, - ), + #[getset(get = "pub")] + pub struct BlockEventFilter { + pub height: Option, + pub status: Option, + } + + #[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Default, + Getters, + Decode, + Encode, + Deserialize, + Serialize, + IntoSchema, + )] + #[ffi_type] + #[getset(get = "pub")] + pub struct TransactionEventFilter { + pub hash: Option>, + #[getset(skip)] + pub block_height: Option>, + pub status: Option, } } -impl PipelineEventFilter { - /// Creates a new [`PipelineEventFilter`] accepting all [`PipelineEvent`]s +impl BlockEventFilter { + /// Match only block with the given height #[must_use] - #[inline] - pub const fn new() -> Self { - Self { - status_kind: None, - entity_kind: None, - hash: None, - } + pub fn for_height(mut self, height: u64) -> Self { + self.height = Some(height); + self } - /// Modifies a [`PipelineEventFilter`] to accept only [`PipelineEvent`]s originating from a specific entity kind (block/transaction). + /// Match only block with the given status #[must_use] - #[inline] - pub const fn for_entity(mut self, entity_kind: PipelineEntityKind) -> Self { - self.entity_kind = Some(entity_kind); + pub fn for_status(mut self, status: BlockStatus) -> Self { + self.status = Some(status); self } +} + +impl TransactionEventFilter { + /// Get height of the block filter is set to track + pub fn block_height(&self) -> Option> { + self.block_height + } - /// Modifies a [`PipelineEventFilter`] to accept only [`PipelineEvent`]s with a specific status. + /// Match only transactions with the given block height #[must_use] - #[inline] - pub const fn for_status(mut self, status_kind: PipelineStatusKind) -> Self { - self.status_kind = Some(status_kind); + pub fn for_block_height(mut self, block_height: Option) -> Self { + self.block_height = Some(block_height); self } - /// Modifies a [`PipelineEventFilter`] to accept only [`PipelineEvent`]s originating from an entity with specified hash. + /// Match only transactions with the given hash #[must_use] - #[inline] - pub const fn for_hash(mut self, hash: Hash) -> Self { + pub fn for_hash(mut self, hash: HashOf) -> Self { self.hash = Some(hash); self } - #[inline] - #[cfg(feature = "transparent_api")] + /// Match only transactions with the given status + #[must_use] + pub fn for_status(mut self, status: TransactionStatus) -> Self { + self.status = Some(status); + self + } +} + +#[cfg(feature = "transparent_api")] +impl TransactionEventFilter { fn field_matches(filter: Option<&T>, event: &T) -> bool { filter.map_or(true, |field| field == event) } } #[cfg(feature = "transparent_api")] -impl super::EventFilter for PipelineEventFilter { - type Event = PipelineEvent; - - /// Check if `self` accepts the `event`. - #[inline] - fn matches(&self, event: &PipelineEvent) -> bool { - [ - Self::field_matches(self.entity_kind.as_ref(), &event.entity_kind), - Self::field_matches(self.status_kind.as_ref(), &event.status.kind()), - Self::field_matches(self.hash.as_ref(), &event.hash), - ] - .into_iter() - .all(core::convert::identity) +impl BlockEventFilter { + fn field_matches(filter: Option<&T>, event: &T) -> bool { + filter.map_or(true, |field| field == event) } } #[cfg(feature = "transparent_api")] -impl PipelineStatus { - fn kind(&self) -> PipelineStatusKind { - PipelineStatusKind::from(self) +impl super::EventFilter for PipelineEventFilterBox { + type Event = PipelineEventBox; + + /// Check if `self` accepts the `event`. + #[inline] + fn matches(&self, event: &PipelineEventBox) -> bool { + match (self, event) { + (Self::Block(block_filter), PipelineEventBox::Block(block_event)) => [ + BlockEventFilter::field_matches( + block_filter.height.as_ref(), + &block_event.header.height, + ), + BlockEventFilter::field_matches(block_filter.status.as_ref(), &block_event.status), + ] + .into_iter() + .all(core::convert::identity), + ( + Self::Transaction(transaction_filter), + PipelineEventBox::Transaction(transaction_event), + ) => [ + TransactionEventFilter::field_matches( + transaction_filter.hash.as_ref(), + &transaction_event.hash, + ), + TransactionEventFilter::field_matches( + transaction_filter.block_height.as_ref(), + &transaction_event.block_height, + ), + TransactionEventFilter::field_matches( + transaction_filter.status.as_ref(), + &transaction_event.status, + ), + ] + .into_iter() + .all(core::convert::identity), + _ => false, + } } } /// Exports common structs and enums from this module. pub mod prelude { pub use super::{ - PipelineEntityKind, PipelineEvent, PipelineEventFilter, PipelineRejectionReason, - PipelineStatus, PipelineStatusKind, + BlockEvent, BlockStatus, PipelineEventBox, PipelineEventFilterBox, TransactionEvent, + TransactionStatus, }; } @@ -235,94 +318,124 @@ mod tests { #[cfg(not(feature = "std"))] use alloc::{string::ToString as _, vec, vec::Vec}; - use super::{super::EventFilter, PipelineRejectionReason::*, *}; + use iroha_crypto::Hash; + + use super::{super::EventFilter, *}; use crate::{transaction::error::TransactionRejectionReason::*, ValidationFail}; + impl BlockHeader { + fn dummy(height: u64) -> Self { + Self { + height, + creation_time_ms: 0, + prev_block_hash: None, + transactions_hash: HashOf::from_untyped_unchecked(Hash::prehashed( + [0_u8; Hash::LENGTH], + )), + view_change_index: 0, + } + } + } + #[test] fn events_are_correctly_filtered() { let events = vec![ - PipelineEvent { - entity_kind: PipelineEntityKind::Transaction, - status: PipelineStatus::Validating, - hash: Hash::prehashed([0_u8; Hash::LENGTH]), - }, - PipelineEvent { - entity_kind: PipelineEntityKind::Transaction, - status: PipelineStatus::Rejected(Transaction(Validation( + TransactionEvent { + hash: HashOf::from_untyped_unchecked(Hash::prehashed([0_u8; Hash::LENGTH])), + block_height: None, + status: TransactionStatus::Queued, + } + .into(), + TransactionEvent { + hash: HashOf::from_untyped_unchecked(Hash::prehashed([0_u8; Hash::LENGTH])), + block_height: Some(3), + status: TransactionStatus::Rejected(Box::new(Validation( ValidationFail::TooComplex, ))), - hash: Hash::prehashed([0_u8; Hash::LENGTH]), - }, - PipelineEvent { - entity_kind: PipelineEntityKind::Transaction, - status: PipelineStatus::Committed, - hash: Hash::prehashed([2_u8; Hash::LENGTH]), - }, - PipelineEvent { - entity_kind: PipelineEntityKind::Block, - status: PipelineStatus::Committed, - hash: Hash::prehashed([2_u8; Hash::LENGTH]), - }, + } + .into(), + TransactionEvent { + hash: HashOf::from_untyped_unchecked(Hash::prehashed([2_u8; Hash::LENGTH])), + block_height: None, + status: TransactionStatus::Approved, + } + .into(), + BlockEvent { + header: BlockHeader::dummy(7), + hash: HashOf::from_untyped_unchecked(Hash::prehashed([7_u8; Hash::LENGTH])), + status: BlockStatus::Committed, + } + .into(), ]; + assert_eq!( + events + .iter() + .filter(|&event| { + let filter: PipelineEventFilterBox = TransactionEventFilter::default() + .for_hash(HashOf::from_untyped_unchecked(Hash::prehashed( + [0_u8; Hash::LENGTH], + ))) + .into(); + + filter.matches(event) + }) + .cloned() + .collect::>(), vec![ - PipelineEvent { - entity_kind: PipelineEntityKind::Transaction, - status: PipelineStatus::Validating, - hash: Hash::prehashed([0_u8; Hash::LENGTH]), - }, - PipelineEvent { - entity_kind: PipelineEntityKind::Transaction, - status: PipelineStatus::Rejected(Transaction(Validation( + TransactionEvent { + hash: HashOf::from_untyped_unchecked(Hash::prehashed([0_u8; Hash::LENGTH])), + block_height: None, + status: TransactionStatus::Queued, + } + .into(), + TransactionEvent { + hash: HashOf::from_untyped_unchecked(Hash::prehashed([0_u8; Hash::LENGTH])), + block_height: Some(3), + status: TransactionStatus::Rejected(Box::new(Validation( ValidationFail::TooComplex, ))), - hash: Hash::prehashed([0_u8; Hash::LENGTH]), - }, + } + .into(), ], - events - .iter() - .filter(|&event| PipelineEventFilter::new() - .for_hash(Hash::prehashed([0_u8; Hash::LENGTH])) - .matches(event)) - .cloned() - .collect::>() ); + assert_eq!( - vec![PipelineEvent { - entity_kind: PipelineEntityKind::Block, - status: PipelineStatus::Committed, - hash: Hash::prehashed([2_u8; Hash::LENGTH]), - }], events .iter() - .filter(|&event| PipelineEventFilter::new() - .for_entity(PipelineEntityKind::Block) - .matches(event)) + .filter(|&event| { + let filter: PipelineEventFilterBox = BlockEventFilter::default().into(); + filter.matches(event) + }) .cloned() - .collect::>() + .collect::>(), + vec![BlockEvent { + status: BlockStatus::Committed, + hash: HashOf::from_untyped_unchecked(Hash::prehashed([7_u8; Hash::LENGTH])), + header: BlockHeader::dummy(7), + } + .into()], ); assert_eq!( - vec![PipelineEvent { - entity_kind: PipelineEntityKind::Transaction, - status: PipelineStatus::Committed, - hash: Hash::prehashed([2_u8; Hash::LENGTH]), - }], events .iter() - .filter(|&event| PipelineEventFilter::new() - .for_entity(PipelineEntityKind::Transaction) - .for_hash(Hash::prehashed([2_u8; Hash::LENGTH])) - .matches(event)) + .filter(|&event| { + let filter: PipelineEventFilterBox = TransactionEventFilter::default() + .for_hash(HashOf::from_untyped_unchecked(Hash::prehashed( + [2_u8; Hash::LENGTH], + ))) + .into(); + + filter.matches(event) + }) .cloned() - .collect::>() + .collect::>(), + vec![TransactionEvent { + hash: HashOf::from_untyped_unchecked(Hash::prehashed([2_u8; Hash::LENGTH])), + block_height: None, + status: TransactionStatus::Approved, + } + .into()], ); - assert_eq!( - events, - events - .iter() - .filter(|&event| PipelineEventFilter::new().matches(event)) - .cloned() - .collect::>() - ) } } diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index 87352bead15..bd004680b5d 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -616,7 +616,6 @@ pub mod parameter { } #[model] -#[allow(irrefutable_let_patterns)] // Triggered from derives macros pub mod model { use super::*; @@ -1022,16 +1021,6 @@ impl From for RangeInclusive { } } -/// Get the current system time as `Duration` since the unix epoch. -#[cfg(feature = "std")] -pub fn current_time() -> core::time::Duration { - use std::time::SystemTime; - - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Failed to get the current system time") -} - declare_versioned_with_scale!(BatchedResponse 1..2, Debug, Clone, iroha_macro::FromVariant, IntoSchema); impl From> for (T, crate::query::cursor::ForwardCursor) { @@ -1086,8 +1075,6 @@ pub mod prelude { pub use iroha_crypto::PublicKey; pub use iroha_primitives::numeric::{numeric, Numeric, NumericSpec}; - #[cfg(feature = "std")] - pub use super::current_time; pub use super::{ account::prelude::*, asset::prelude::*, domain::prelude::*, events::prelude::*, executor::prelude::*, isi::prelude::*, metadata::prelude::*, name::prelude::*, diff --git a/data_model/src/query/mod.rs b/data_model/src/query/mod.rs index 7fdb0b9a149..14c7b6caecf 100644 --- a/data_model/src/query/mod.rs +++ b/data_model/src/query/mod.rs @@ -144,7 +144,7 @@ pub mod model { )] #[enum_ref(derive(Encode, FromVariant))] #[strum_discriminants( - vis(pub(crate)), + vis(pub(super)), name(QueryType), derive(Encode), allow(clippy::enum_variant_names) diff --git a/data_model/src/query/predicate.rs b/data_model/src/query/predicate.rs index 6421c164457..7892ed6acea 100644 --- a/data_model/src/query/predicate.rs +++ b/data_model/src/query/predicate.rs @@ -1163,7 +1163,7 @@ pub mod value { QueryOutputPredicate::Display(pred) => pred.applies(&input.to_string()), QueryOutputPredicate::TimeStamp(pred) => match input { QueryOutputBox::Block(block) => { - pred.applies(block.header().timestamp().as_millis()) + pred.applies(block.header().creation_time().as_millis()) } _ => false, }, diff --git a/data_model/src/smart_contract.rs b/data_model/src/smart_contract.rs index 379da0585d9..5d51c2f89d3 100644 --- a/data_model/src/smart_contract.rs +++ b/data_model/src/smart_contract.rs @@ -20,7 +20,7 @@ pub mod payloads { /// Trigger owner who registered the trigger pub owner: AccountId, /// Event which triggered the execution - pub event: Event, + pub event: EventBox, } /// Payload for migrate entrypoint diff --git a/data_model/src/transaction.rs b/data_model/src/transaction.rs index fbfded831ab..75f34b5c4d2 100644 --- a/data_model/src/transaction.rs +++ b/data_model/src/transaction.rs @@ -9,7 +9,6 @@ use core::{ }; use derive_more::{DebugCustom, Display}; -use getset::Getters; use iroha_crypto::SignaturesOf; use iroha_data_model_derive::model; use iroha_macro::FromVariant; @@ -28,6 +27,8 @@ use crate::{ #[model] pub mod model { + use getset::{CopyGetters, Getters}; + use super::*; /// Either ISI or Wasm binary @@ -89,35 +90,26 @@ pub mod model { Eq, PartialOrd, Ord, - Getters, Decode, Encode, Deserialize, Serialize, IntoSchema, )] - #[getset(get = "pub")] - #[ffi_type] - pub struct TransactionPayload { + pub(crate) struct TransactionPayload { /// Unique id of the blockchain. Used for simple replay attack protection. - #[getset(skip)] // FIXME: ffi error pub chain_id: ChainId, /// Creation timestamp (unix time in milliseconds). - #[getset(skip)] pub creation_time_ms: u64, /// Account ID of transaction creator. pub authority: AccountId, /// ISI or a `WebAssembly` smart contract. pub instructions: Executable, /// If transaction is not committed by this time it will be dropped. - #[getset(skip)] pub time_to_live_ms: Option, /// Random value to make different hashes for transactions which occur repeatedly and simultaneously. - // TODO: Only temporary - #[getset(skip)] pub nonce: Option, /// Store for additional information. - #[getset(skip)] // FIXME: ffi error pub metadata: UnlimitedMetadata, } @@ -131,7 +123,7 @@ pub mod model { Eq, PartialOrd, Ord, - Getters, + CopyGetters, Decode, Encode, Deserialize, @@ -139,7 +131,7 @@ pub mod model { IntoSchema, )] #[display(fmt = "{max_instruction_number},{max_wasm_size_bytes}_TL")] - #[getset(get = "pub")] + #[getset(get_copy = "pub")] #[ffi_type] pub struct TransactionLimits { /// Maximum number of instructions per transaction @@ -251,14 +243,14 @@ impl SignedTransaction { #[inline] pub fn instructions(&self) -> &Executable { let SignedTransaction::V1(tx) = self; - tx.payload.instructions() + &tx.payload.instructions } /// Return transaction authority #[inline] pub fn authority(&self) -> &AccountId { let SignedTransaction::V1(tx) = self; - tx.payload.authority() + &tx.payload.authority } /// Return transaction metadata. @@ -449,6 +441,8 @@ pub mod error { #[model] pub mod model { + use getset::Getters; + use super::*; /// Error which indicates max instruction count was reached @@ -565,8 +559,6 @@ pub mod error { InstructionExecution(#[cfg_attr(feature = "std", source)] InstructionExecutionFail), /// Failure in WebAssembly execution WasmExecution(#[cfg_attr(feature = "std", source)] WasmExecutionFail), - /// Transaction rejected due to being expired - Expired, } } @@ -638,7 +630,11 @@ mod http { #[inline] #[cfg(feature = "std")] pub fn new(chain_id: ChainId, authority: AccountId) -> Self { - let creation_time_ms = crate::current_time() + use std::time::SystemTime; + + let creation_time_ms = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Failed to get the current system time") .as_millis() .try_into() .expect("Unix timestamp exceedes u64::MAX"); @@ -738,13 +734,15 @@ pub mod prelude { #[cfg(feature = "http")] pub use super::http::TransactionBuilder; pub use super::{ - error::prelude::*, Executable, SignedTransaction, TransactionPayload, TransactionValue, - WasmSmartContract, + error::prelude::*, Executable, SignedTransaction, TransactionValue, WasmSmartContract, }; } #[cfg(test)] mod tests { + #[cfg(not(feature = "std"))] + use alloc::vec; + use super::*; #[test] diff --git a/data_model/src/trigger.rs b/data_model/src/trigger.rs index e65cf5f4a96..9a739cfead1 100644 --- a/data_model/src/trigger.rs +++ b/data_model/src/trigger.rs @@ -237,21 +237,21 @@ pub mod action { impl PartialOrd for Action { fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl Ord for Action { + fn cmp(&self, other: &Self) -> cmp::Ordering { // Exclude the executable. When debugging and replacing // the trigger, its position in Hash and Tree maps should // not change depending on the content. match self.repeats.cmp(&other.repeats) { cmp::Ordering::Equal => {} - ord => return Some(ord), + ord => return ord, } - Some(self.authority.cmp(&other.authority)) - } - } - impl Ord for Action { - fn cmp(&self, other: &Self) -> cmp::Ordering { - self.partial_cmp(other) - .expect("`PartialCmp::partial_cmp()` for `Action` should never return `None`") + self.authority.cmp(&other.authority) } } diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index b6d93a29fdc..3bcbf61b28d 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -480,6 +480,34 @@ } ] }, + "BlockEvent": { + "Struct": [ + { + "name": "header", + "type": "BlockHeader" + }, + { + "name": "hash", + "type": "HashOf" + }, + { + "name": "status", + "type": "BlockStatus" + } + ] + }, + "BlockEventFilter": { + "Struct": [ + { + "name": "height", + "type": "Option" + }, + { + "name": "status", + "type": "Option" + } + ] + }, "BlockHeader": { "Struct": [ { @@ -487,24 +515,20 @@ "type": "u64" }, { - "name": "timestamp_ms", + "name": "creation_time_ms", "type": "u64" }, { - "name": "previous_block_hash", + "name": "prev_block_hash", "type": "Option>" }, { "name": "transactions_hash", - "type": "Option>>" + "type": "HashOf>" }, { "name": "view_change_index", "type": "u64" - }, - { - "name": "consensus_estimation_ms", - "type": "u64" } ] }, @@ -525,7 +549,7 @@ }, { "name": "event_recommendations", - "type": "Vec" + "type": "Vec" } ] }, @@ -537,6 +561,27 @@ } ] }, + "BlockStatus": { + "Enum": [ + { + "tag": "Approved", + "discriminant": 0 + }, + { + "tag": "Rejected", + "discriminant": 1, + "type": "BlockRejectionReason" + }, + { + "tag": "Committed", + "discriminant": 2 + }, + { + "tag": "Applied", + "discriminant": 3 + } + ] + }, "BlockSubscriptionRequest": "NonZero", "Burn": { "Struct": [ @@ -857,12 +902,12 @@ "u32" ] }, - "Event": { + "EventBox": { "Enum": [ { "tag": "Pipeline", "discriminant": 0, - "type": "PipelineEvent" + "type": "PipelineEventBox" }, { "tag": "Data", @@ -891,7 +936,7 @@ { "tag": "Pipeline", "discriminant": 0, - "type": "PipelineEventFilter" + "type": "PipelineEventFilterBox" }, { "tag": "Data", @@ -915,8 +960,8 @@ } ] }, - "EventMessage": "Event", - "EventSubscriptionRequest": "EventFilterBox", + "EventMessage": "EventBox", + "EventSubscriptionRequest": "Vec", "Executable": { "Enum": [ { @@ -2229,21 +2274,21 @@ "Option": { "Option": "AssetId" }, + "Option": { + "Option": "BlockStatus" + }, "Option": { "Option": "DomainId" }, "Option": { "Option": "Duration" }, - "Option": { - "Option": "Hash" - }, - "Option>>": { - "Option": "HashOf>" - }, "Option>": { "Option": "HashOf" }, + "Option>": { + "Option": "HashOf" + }, "Option": { "Option": "IpfsPath" }, @@ -2253,18 +2298,15 @@ "Option>": { "Option": "NonZero" }, + "Option>": { + "Option": "Option" + }, "Option": { "Option": "ParameterId" }, "Option": { "Option": "PeerId" }, - "Option": { - "Option": "PipelineEntityKind" - }, - "Option": { - "Option": "PipelineStatusKind" - }, "Option": { "Option": "RoleId" }, @@ -2277,6 +2319,9 @@ "Option": { "Option": "TransactionRejectionReason" }, + "Option": { + "Option": "TransactionStatus" + }, "Option": { "Option": "TriggerCompletedOutcomeType" }, @@ -2286,6 +2331,9 @@ "Option": { "Option": "u32" }, + "Option": { + "Option": "u64" + }, "Parameter": { "Struct": [ { @@ -2413,94 +2461,31 @@ } ] }, - "PipelineEntityKind": { + "PipelineEventBox": { "Enum": [ - { - "tag": "Block", - "discriminant": 0 - }, { "tag": "Transaction", - "discriminant": 1 - } - ] - }, - "PipelineEvent": { - "Struct": [ - { - "name": "entity_kind", - "type": "PipelineEntityKind" - }, - { - "name": "status", - "type": "PipelineStatus" - }, - { - "name": "hash", - "type": "Hash" - } - ] - }, - "PipelineEventFilter": { - "Struct": [ - { - "name": "entity_kind", - "type": "Option" - }, - { - "name": "status_kind", - "type": "Option" - }, - { - "name": "hash", - "type": "Option" - } - ] - }, - "PipelineRejectionReason": { - "Enum": [ - { - "tag": "Block", "discriminant": 0, - "type": "BlockRejectionReason" + "type": "TransactionEvent" }, { - "tag": "Transaction", + "tag": "Block", "discriminant": 1, - "type": "TransactionRejectionReason" + "type": "BlockEvent" } ] }, - "PipelineStatus": { + "PipelineEventFilterBox": { "Enum": [ { - "tag": "Validating", - "discriminant": 0 + "tag": "Transaction", + "discriminant": 0, + "type": "TransactionEventFilter" }, { - "tag": "Rejected", + "tag": "Block", "discriminant": 1, - "type": "PipelineRejectionReason" - }, - { - "tag": "Committed", - "discriminant": 2 - } - ] - }, - "PipelineStatusKind": { - "Enum": [ - { - "tag": "Validating", - "discriminant": 0 - }, - { - "tag": "Rejected", - "discriminant": 1 - }, - { - "tag": "Committed", - "discriminant": 2 + "type": "BlockEventFilter" } ] }, @@ -3574,6 +3559,38 @@ } ] }, + "TransactionEvent": { + "Struct": [ + { + "name": "hash", + "type": "HashOf" + }, + { + "name": "block_height", + "type": "Option" + }, + { + "name": "status", + "type": "TransactionStatus" + } + ] + }, + "TransactionEventFilter": { + "Struct": [ + { + "name": "hash", + "type": "Option>" + }, + { + "name": "block_height", + "type": "Option>" + }, + { + "name": "status", + "type": "Option" + } + ] + }, "TransactionLimitError": { "Struct": [ { @@ -3664,10 +3681,27 @@ "tag": "WasmExecution", "discriminant": 4, "type": "WasmExecutionFail" + } + ] + }, + "TransactionStatus": { + "Enum": [ + { + "tag": "Queued", + "discriminant": 0 }, { "tag": "Expired", - "discriminant": 5 + "discriminant": 1 + }, + { + "tag": "Approved", + "discriminant": 2 + }, + { + "tag": "Rejected", + "discriminant": 3, + "type": "TransactionRejectionReason" } ] }, @@ -3893,7 +3927,7 @@ { "tag": "Pipeline", "discriminant": 0, - "type": "PipelineEventFilter" + "type": "PipelineEventFilterBox" }, { "tag": "Data", @@ -4061,8 +4095,11 @@ } ] }, - "Vec": { - "Vec": "Event" + "Vec": { + "Vec": "EventBox" + }, + "Vec": { + "Vec": "EventFilterBox" }, "Vec>": { "Vec": "GenericPredicateBox" diff --git a/logger/Cargo.toml b/logger/Cargo.toml index 83aba591aea..4c26029f91b 100644 --- a/logger/Cargo.toml +++ b/logger/Cargo.toml @@ -32,6 +32,6 @@ tokio = { workspace = true, features = ["macros", "time", "rt"] } [features] -tokio-console = ["dep:console-subscriber", "tokio/tracing", "iroha_config/tokio-console"] -# Workaround to avoid activating `tokio-console` with `--all-features` flag, because `tokio-console` require `tokio_unstable` rustc flag -no-tokio-console = [] +tokio_console = ["dep:console-subscriber", "tokio/tracing", "iroha_config/tokio_console"] +# Workaround to avoid activating `tokio_console` with `--all-features` flag, because `tokio_console` require `tokio_unstable` rustc flag +no_tokio_console = [] diff --git a/logger/src/lib.rs b/logger/src/lib.rs index 87a5ac3ed60..bfdcbd2b796 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -82,7 +82,7 @@ pub fn test_logger() -> LoggerHandle { // with ENV vars rather than by extending `test_logger` signature. This will both remain // `test_logger` simple and also will emphasise isolation which is necessary anyway in // case of singleton mocking (where the logger is the singleton). - #[allow(clippy::needless_update)] // triggers without "tokio-console" feature + #[allow(clippy::needless_update)] // triggers without "tokio_console" feature let config = Config { level: Level::DEBUG, format: Format::Pretty, @@ -118,7 +118,7 @@ where .with(level_filter) .with(tracing_error::ErrorLayer::default()); - #[cfg(all(feature = "tokio-console", not(feature = "no-tokio-console")))] + #[cfg(all(feature = "tokio_console", not(feature = "no_tokio_console")))] let subscriber = { let console_subscriber = console_subscriber::ConsoleLayer::builder() .server_addr( diff --git a/schema/gen/src/lib.rs b/schema/gen/src/lib.rs index ebfb956f436..04d20a776f4 100644 --- a/schema/gen/src/lib.rs +++ b/schema/gen/src/lib.rs @@ -96,13 +96,17 @@ types!( BTreeSet>, BatchedResponse, BatchedResponseV1, + BlockEvent, + BlockEventFilter, BlockHeader, BlockMessage, BlockPayload, BlockRejectionReason, + BlockStatus, BlockSubscriptionRequest, Box>, Box, + Box, Burn, Burn, Burn, @@ -124,7 +128,7 @@ types!( DomainId, DomainOwnerChanged, Duration, - Event, + EventBox, EventMessage, EventSubscriptionRequest, Executable, @@ -232,25 +236,26 @@ types!( Numeric, NumericSpec, Option, + Option, Option, Option, Option, + Option, Option, Option, - Option, - Option>>, Option>, + Option>, Option, Option, Option, + Option>, Option, Option, - Option, - Option, Option, Option, Option, Option, + Option, Option, Option, Parameter, @@ -265,12 +270,8 @@ types!( PermissionToken, PermissionTokenSchema, PermissionTokenSchemaUpdateEvent, - PipelineEntityKind, - PipelineEvent, - PipelineEventFilter, - PipelineRejectionReason, - PipelineStatus, - PipelineStatusKind, + PipelineEventBox, + PipelineEventFilterBox, PredicateBox, PublicKey, QueryBox, @@ -338,12 +339,15 @@ types!( TimeEventFilter, TimeInterval, TimeSchedule, + TransactionEvent, + TransactionEventFilter, TransactionLimitError, TransactionLimits, TransactionPayload, TransactionQueryOutput, TransactionRejectionReason, TransactionValue, + TransactionStatus, Transfer, Transfer, Transfer, @@ -372,7 +376,8 @@ types!( UnregisterBox, Upgrade, ValidationFail, - Vec, + Vec, + Vec, Vec, Vec, Vec, @@ -412,6 +417,7 @@ mod tests { BlockHeader, BlockPayload, SignedBlock, SignedBlockV1, }, domain::NewDomain, + events::pipeline::{BlockEventFilter, TransactionEventFilter}, executor::Executor, ipfs::IpfsPath, isi::{ @@ -435,7 +441,10 @@ mod tests { }, ForwardCursor, QueryOutputBox, }, - transaction::{error::TransactionLimitError, SignedTransactionV1, TransactionLimits}, + transaction::{ + error::TransactionLimitError, SignedTransactionV1, TransactionLimits, + TransactionPayload, + }, BatchedResponse, BatchedResponseV1, Level, }; use iroha_primitives::{ diff --git a/smart_contract/executor/src/default.rs b/smart_contract/executor/src/default.rs index c409a082878..50b951d85a8 100644 --- a/smart_contract/executor/src/default.rs +++ b/smart_contract/executor/src/default.rs @@ -1246,7 +1246,7 @@ pub mod role { let role_id = $isi.object(); let find_role_query_res = match FindRoleByRoleId::new(role_id.clone()).execute() { - Ok(res) => res.into_raw_parts().0, + Ok(res) => res.into_parts().0, Err(error) => { deny!($executor, error); } diff --git a/smart_contract/executor/src/permission.rs b/smart_contract/executor/src/permission.rs index 1bcbb66489f..270048f2140 100644 --- a/smart_contract/executor/src/permission.rs +++ b/smart_contract/executor/src/permission.rs @@ -139,7 +139,7 @@ pub mod asset_definition { ) -> Result { let asset_definition = FindAssetDefinitionById::new(asset_definition_id.clone()) .execute() - .map(QueryOutputCursor::into_raw_parts) + .map(QueryOutputCursor::into_parts) .map(|(batch, _cursor)| batch)?; if asset_definition.owned_by() == authority { Ok(true) @@ -226,7 +226,7 @@ pub mod trigger { pub fn is_trigger_owner(trigger_id: &TriggerId, authority: &AccountId) -> Result { let trigger = FindTriggerById::new(trigger_id.clone()) .execute() - .map(QueryOutputCursor::into_raw_parts) + .map(QueryOutputCursor::into_parts) .map(|(batch, _cursor)| batch)?; if trigger.action().authority() == authority { Ok(true) @@ -271,7 +271,7 @@ pub mod domain { pub fn is_domain_owner(domain_id: &DomainId, authority: &AccountId) -> Result { FindDomainById::new(domain_id.clone()) .execute() - .map(QueryOutputCursor::into_raw_parts) + .map(QueryOutputCursor::into_parts) .map(|(batch, _cursor)| batch) .map(|domain| domain.owned_by() == authority) } diff --git a/smart_contract/src/lib.rs b/smart_contract/src/lib.rs index b6663381a2b..576f1915ccb 100644 --- a/smart_contract/src/lib.rs +++ b/smart_contract/src/lib.rs @@ -291,7 +291,7 @@ pub struct QueryOutputCursor { impl QueryOutputCursor { /// Get inner values of batch and cursor, consuming [`Self`]. - pub fn into_raw_parts(self) -> (T, ForwardCursor) { + pub fn into_parts(self) -> (T, ForwardCursor) { (self.batch, self.cursor) } } @@ -524,7 +524,7 @@ mod tests { let response: Result, ValidationFail> = Ok(BatchedResponseV1::new( - QUERY_RESULT.unwrap().into_raw_parts().0, + QUERY_RESULT.unwrap().into_parts().0, ForwardCursor::new(None, None), ) .into()); diff --git a/telemetry/Cargo.toml b/telemetry/Cargo.toml index e1fb573541b..4cfb5b43f1c 100644 --- a/telemetry/Cargo.toml +++ b/telemetry/Cargo.toml @@ -13,9 +13,9 @@ workspace = true [features] # Support developer-specific telemetry. # Should not be enabled on production builds. -dev-telemetry = [] +dev_telemetry = [] # Export Prometheus metrics. See https://prometheus.io/. -metric-instrumentation = [] +metric_instrumentation = [] [dependencies] iroha_config = { workspace = true } diff --git a/telemetry/derive/src/lib.rs b/telemetry/derive/src/lib.rs index 471260e9ee3..4263aa0c983 100644 --- a/telemetry/derive/src/lib.rs +++ b/telemetry/derive/src/lib.rs @@ -9,9 +9,9 @@ use syn::{parse::Parse, punctuated::Punctuated, token::Comma, FnArg, LitStr, Pat // TODO: export these as soon as proc-macro crates are able to export // anything other than proc-macros. -#[cfg(feature = "metric-instrumentation")] +#[cfg(feature = "metric_instrumentation")] const TOTAL_STR: &str = "total"; -#[cfg(feature = "metric-instrumentation")] +#[cfg(feature = "metric_instrumentation")] const SUCCESS_STR: &str = "success"; fn type_has_metrics_field(ty: &Type) -> bool { @@ -79,7 +79,7 @@ impl Parse for MetricSpecs { } struct MetricSpec { - #[cfg(feature = "metric-instrumentation")] + #[cfg(feature = "metric_instrumentation")] timing: bool, metric_name: LitStr, } @@ -108,7 +108,7 @@ impl Parse for MetricSpec { }; Ok(Self { metric_name, - #[cfg(feature = "metric-instrumentation")] + #[cfg(feature = "metric_instrumentation")] timing: _timing, }) } @@ -236,17 +236,22 @@ fn impl_metrics(emitter: &mut Emitter, _specs: &MetricSpecs, func: &syn::ItemFn) } }; - #[cfg(feature = "metric-instrumentation")] + #[cfg(feature = "metric_instrumentation")] let res = { let (totals, successes, times) = write_metrics(_metric_arg_ident, _specs); quote!( #(#attrs)* #vis #sig { let _closure = || #block; + let start_time = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("Failed to get the current system time"); - let start_time = #_metric_arg_ident.metrics.current_time(); #totals let res = _closure(); - let end_time = #_metric_arg_ident.metrics.current_time(); + let end_time = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("Failed to get the current system time"); + #times if let Ok(_) = res { #successes @@ -255,7 +260,7 @@ fn impl_metrics(emitter: &mut Emitter, _specs: &MetricSpecs, func: &syn::ItemFn) }); }; - #[cfg(not(feature = "metric-instrumentation"))] + #[cfg(not(feature = "metric_instrumentation"))] let res = quote!( #(#attrs)* #vis #sig { #block @@ -264,7 +269,7 @@ fn impl_metrics(emitter: &mut Emitter, _specs: &MetricSpecs, func: &syn::ItemFn) res } -#[cfg(feature = "metric-instrumentation")] +#[cfg(feature = "metric_instrumentation")] fn write_metrics( metric_arg_ident: proc_macro2::Ident, specs: MetricSpecs, diff --git a/telemetry/src/lib.rs b/telemetry/src/lib.rs index 0fb3ec02ebd..9f2ba7e72f3 100644 --- a/telemetry/src/lib.rs +++ b/telemetry/src/lib.rs @@ -1,6 +1,6 @@ //! Crate with iroha telemetry processing -#[cfg(feature = "dev-telemetry")] +#[cfg(feature = "dev_telemetry")] pub mod dev; pub mod futures; pub mod metrics; diff --git a/telemetry/src/metrics.rs b/telemetry/src/metrics.rs index 404e32c3916..7e93b02f94f 100644 --- a/telemetry/src/metrics.rs +++ b/telemetry/src/metrics.rs @@ -1,9 +1,6 @@ //! [`Metrics`] and [`Status`]-related logic and functions. -use std::{ - ops::Deref, - time::{Duration, SystemTime}, -}; +use std::{ops::Deref, time::Duration}; use parity_scale_codec::{Compact, Decode, Encode}; use prometheus::{ @@ -218,17 +215,6 @@ impl Metrics { Encoder::encode(&encoder, &metric_families, &mut buffer)?; Ok(String::from_utf8(buffer)?) } - - /// Get time elapsed since Unix epoch. - /// - /// # Panics - /// Never - #[allow(clippy::unused_self)] - pub fn current_time(&self) -> Duration { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Failed to get the current system time") - } } #[cfg(test)] diff --git a/tools/parity_scale_decoder/Cargo.toml b/tools/parity_scale_decoder/Cargo.toml index 37087501892..909795cff49 100644 --- a/tools/parity_scale_decoder/Cargo.toml +++ b/tools/parity_scale_decoder/Cargo.toml @@ -13,7 +13,7 @@ workspace = true [features] # Disable colour for all program output. # Useful for Docker-based deployment and terminals without colour support. -no-color = ["colored/no-color"] +no_color = ["colored/no-color"] [dependencies] iroha_data_model = { workspace = true, features = ["http"] } diff --git a/tools/parity_scale_decoder/README.md b/tools/parity_scale_decoder/README.md index a8390a6ef67..fc9fe7887f5 100644 --- a/tools/parity_scale_decoder/README.md +++ b/tools/parity_scale_decoder/README.md @@ -13,7 +13,7 @@ cargo build --bin parity_scale_decoder If your terminal does not support colours, run: ```bash -cargo build --features no-color --bin parity_scale_decoder +cargo build --features no_color --bin parity_scale_decoder ``` ## Usage @@ -66,9 +66,9 @@ Decode the data type from a given binary. ``` * If you are not sure which data type is encoded in the binary, run the tool without the `--type` option: - + ```bash - ./target/debug/parity_scale_decoder decode + ./target/debug/parity_scale_decoder decode ``` ### `decode` usage examples diff --git a/tools/parity_scale_decoder/src/main.rs b/tools/parity_scale_decoder/src/main.rs index 9bf3a824693..da17ddd92b9 100644 --- a/tools/parity_scale_decoder/src/main.rs +++ b/tools/parity_scale_decoder/src/main.rs @@ -21,6 +21,7 @@ use iroha_data_model::{ BlockHeader, BlockPayload, SignedBlock, SignedBlockV1, }, domain::NewDomain, + events::pipeline::{BlockEventFilter, TransactionEventFilter}, executor::Executor, ipfs::IpfsPath, isi::{ @@ -44,7 +45,9 @@ use iroha_data_model::{ }, ForwardCursor, QueryOutputBox, }, - transaction::{error::TransactionLimitError, SignedTransactionV1, TransactionLimits}, + transaction::{ + error::TransactionLimitError, SignedTransactionV1, TransactionLimits, TransactionPayload, + }, BatchedResponse, BatchedResponseV1, Level, }; use iroha_primitives::{ diff --git a/torii/src/event.rs b/torii/src/event.rs index 873f81d91ec..ee9d72757b0 100644 --- a/torii/src/event.rs +++ b/torii/src/event.rs @@ -44,7 +44,7 @@ pub type Result = core::result::Result; #[derive(Debug)] pub struct Consumer { stream: WebSocket, - filter: EventFilterBox, + filters: Vec, } impl Consumer { @@ -54,8 +54,8 @@ impl Consumer { /// Can fail due to timeout or without message at websocket or during decoding request #[iroha_futures::telemetry_future] pub async fn new(mut stream: WebSocket) -> Result { - let EventSubscriptionRequest(filter) = stream.recv().await?; - Ok(Consumer { stream, filter }) + let EventSubscriptionRequest(filters) = stream.recv().await?; + Ok(Consumer { stream, filters }) } /// Forwards the `event` over the `stream` if it matches the `filter`. @@ -63,8 +63,8 @@ impl Consumer { /// # Errors /// Can fail due to timeout or sending event. Also receiving might fail #[iroha_futures::telemetry_future] - pub async fn consume(&mut self, event: Event) -> Result<()> { - if !self.filter.matches(&event) { + pub async fn consume(&mut self, event: EventBox) -> Result<()> { + if !self.filters.iter().any(|filter| filter.matches(&event)) { return Ok(()); }