From 5b5db88c31d11b77531ba28d2ce6d222c3d9a3ca 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 | 2 +- cli/README.md | 2 +- cli/src/lib.rs | 2 +- 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 | 27 +- client_cli/src/main.rs | 13 +- config/tests/fixtures/full.toml | 2 +- configs/peer.template.toml | 2 +- configs/swarm/executor.wasm | Bin 533713 -> 535234 bytes core/benches/blocks/apply_blocks.rs | 10 +- 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 | 322 +++++++++---- core/src/block_sync.rs | 27 +- core/src/kura.rs | 6 +- core/src/lib.rs | 6 +- core/src/queue.rs | 142 ++++-- core/src/smartcontracts/isi/query.rs | 12 +- core/src/smartcontracts/isi/triggers/set.rs | 32 +- .../isi/triggers/specialized.rs | 4 +- core/src/smartcontracts/wasm.rs | 4 +- core/src/state.rs | 83 ++-- core/src/sumeragi/main_loop.rs | 259 +++++----- core/src/sumeragi/message.rs | 16 +- core/src/sumeragi/mod.rs | 43 +- core/src/tx.rs | 2 +- core/test_network/src/lib.rs | 14 +- crypto/src/lib.rs | 7 - crypto/src/signature/mod.rs | 2 +- data_model/derive/src/enum_ref.rs | 2 +- data_model/derive/src/lib.rs | 56 ++- data_model/derive/src/model.rs | 8 +- data_model/src/account.rs | 16 +- data_model/src/asset.rs | 2 +- data_model/src/block.rs | 76 +-- data_model/src/domain.rs | 2 +- data_model/src/events/data/events.rs | 16 +- data_model/src/events/data/filters.rs | 3 +- data_model/src/events/execute_trigger.rs | 2 +- data_model/src/events/mod.rs | 126 +++-- data_model/src/events/pipeline.rs | 455 +++++++++++------- data_model/src/events/time.rs | 2 +- data_model/src/events/trigger_completed.rs | 2 +- data_model/src/executor.rs | 2 +- data_model/src/ipfs.rs | 2 +- data_model/src/isi.rs | 4 +- data_model/src/lib.rs | 17 +- data_model/src/metadata.rs | 2 +- data_model/src/name.rs | 2 +- data_model/src/peer.rs | 2 +- data_model/src/permission.rs | 2 +- data_model/src/query/cursor.rs | 2 +- data_model/src/query/mod.rs | 10 +- data_model/src/query/sorting.rs | 2 +- data_model/src/role.rs | 2 +- data_model/src/smart_contract.rs | 2 +- data_model/src/transaction.rs | 48 +- data_model/src/trigger.rs | 20 +- docs/source/references/schema.json | 242 ++++++---- primitives/src/must_use.rs | 22 +- schema/gen/src/lib.rs | 34 +- smart_contract/executor/src/default.rs | 2 +- smart_contract/executor/src/permission.rs | 6 +- smart_contract/src/lib.rs | 4 +- telemetry/derive/src/lib.rs | 9 +- telemetry/src/dev.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 +- 89 files changed, 1519 insertions(+), 1060 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6bc087b1ba..406b3f294af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/README.md b/cli/README.md index 5ba8d269b39..60a69f453c9 100644 --- a/cli/README.md +++ b/cli/README.md @@ -22,7 +22,7 @@ The results of the compilation can be found in `/target/release ### Add features -To add optional features, use ``--features``. For example, to add the support for _dev_telemetry_, run: +To add optional features, use ``--features``. For example, to add the support for _dev telemetry_, run: ```bash cargo build --release --features dev-telemetry diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 1c3b107bcc6..9854535d50a 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -252,7 +252,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())); #[cfg(feature = "telemetry")] Self::start_telemetry(&logger, &config).await?; 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..dbb738c7880 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.into_iter().map(Into::into).collect(), 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: Vec, headers: HashMap, url: Url, ) -> Result { Ok(Self { - filter, - headers, url: transform_ws_url(url)?, + headers, + filters, }) } } @@ -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..8a9bb9fb034 100644 --- a/client/tests/integration/triggers/time_trigger.rs +++ b/client/tests/integration/triggers/time_trigger.rs @@ -6,11 +6,20 @@ use iroha_client::{ data_model::{prelude::*, transaction::WasmSmartContract}, }; use iroha_config::parameters::defaults::chain_wide::DEFAULT_CONSENSUS_ESTIMATION; +use iroha_data_model::events::pipeline::{BlockEventFilter, BlockStatus}; use iroha_logger::info; use test_network::*; use crate::integration::new_account_with_random_public_key; +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 +42,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 +75,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 +101,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 +229,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 +281,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 +299,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/tests/fixtures/full.toml b/config/tests/fixtures/full.toml index ef611ea97cf..aae107333d0 100644 --- a/config/tests/fixtures/full.toml +++ b/config/tests/fixtures/full.toml @@ -57,7 +57,7 @@ min_retry_period = 5_000 max_retry_delay_exponent = 4 [dev_telemetry] -out_file = "./dev-telemetry.json" +out_file = "./dev_telemetry.json" [chain_wide] max_transactions_in_block = 512 diff --git a/configs/peer.template.toml b/configs/peer.template.toml index aa8691bcf76..2c8b88a7616 100644 --- a/configs/peer.template.toml +++ b/configs/peer.template.toml @@ -63,4 +63,4 @@ [dev_telemetry] ## A path to a file with JSON logs -# out_file = "./dev-telemetry.json" +# out_file = "./dev_telemetry.json" diff --git a/configs/swarm/executor.wasm b/configs/swarm/executor.wasm index fb05db1652ebebd21b800200616392a24d65d64b..c6700b3d1e4b9fd734494f8e48c78e8b792f470e 100644 GIT binary patch delta 137123 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 O{x6yjTJHbu@c#!f>MHaA delta 135642 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 diff --git a/core/benches/blocks/apply_blocks.rs b/core/benches/blocks/apply_blocks.rs index f85921695d5..bdf75e9e215 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 _events = state_block.apply_without_execution(&block); state_block.commit(); - Ok(block) + block }) - .collect::, _>>()? + .collect::>() }; Ok(Self { state, blocks }) @@ -68,7 +68,7 @@ impl StateApplyBlocks { pub fn measure(Self { state, blocks }: &Self) -> Result<()> { for (block, i) in blocks.iter().zip(1..) { let mut state_block = state.block(); - state_block.apply(block)?; + let _events = state_block.apply(block)?; assert_eq!(state_block.height(), i); state_block.commit(); } 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..ac6de7fa5d5 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 _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..c7b66b0f718 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -18,6 +18,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}; @@ -51,6 +52,13 @@ pub enum BlockValidationError { /// 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 +101,8 @@ pub enum SignatureVerificationError { pub struct BlockBuilder(B); mod pending { + use std::time::SystemTime; + use iroha_data_model::transaction::TransactionValue; use super::*; @@ -110,7 +120,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 +133,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 +146,29 @@ 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() + height: previous_height + 1, + previous_block_hash: prev_block_hash, + transactions_hash: transactions + .iter() + .map(|value| value.as_ref().hash()) + .collect::>() + .hash(), + timestamp_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"), + view_change_index, consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION .as_millis() .try_into() .expect("Time should fit into u64"), - height: previous_height + 1, - view_change_index, - previous_block_hash, - transactions_hash: transactions - .iter() - .map(|value| value.as_ref().hash()) - .collect::>() - .hash(), } } @@ -222,16 +234,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 +257,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 +276,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 +284,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 +308,51 @@ mod valid { let actual_height = block.header().height; if expected_block_height != actual_height { - return Err(( + return WithEvents::new(Err(( block, BlockValidationError::LatestBlockHeightMismatch { 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().previous_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, + 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 +397,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 +449,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 +481,22 @@ mod valid { pub(crate) fn new_dummy() -> Self { BlockBuilder(Chained(BlockPayload { header: BlockHeader { + height: 2, + previous_block_hash: None, + transactions_hash: None, timestamp_ms: 0, + view_change_index: 0, consensus_estimation_ms: DEFAULT_CONSENSUS_ESTIMATION .as_millis() .try_into() - .expect("Should never overflow?"), - height: 2, - view_change_index: 0, - previous_block_hash: None, - transactions_hash: None, + .expect("Time should fit into u64"), }, 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 +667,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 +681,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 +798,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 +842,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 +917,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 +987,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..69e1cdcbecd 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().previous_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..d5ab05b54b7 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,17 +8,21 @@ 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? - fn check_signature_condition(&self, state_view: &StateView<'_>) -> MustUse { + #[must_use] + fn check_signature_condition(&self, state_view: &StateView<'_>) -> bool { let authority = self.as_ref().authority(); let transaction_signatories = self @@ -34,7 +38,7 @@ impl AcceptedTransaction { .map_account(authority, |account| { account.check_signature_check_condition(&transaction_signatories) }) - .unwrap_or(MustUse(false)) + .unwrap_or(false) } /// Check if [`self`] is committed or rejected. @@ -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. @@ -167,7 +179,7 @@ impl Queue { Err(Error::Expired) } else if tx.is_in_blockchain(state_view) { Err(Error::InBlockchain) - } else if !tx.check_signature_condition(state_view).into_inner() { + } else if !tx.check_signature_condition(state_view) { Err(Error::SignatureCondition) } else { Ok(()) @@ -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. @@ -368,7 +392,6 @@ pub mod tests { use std::{str::FromStr, sync::Arc, thread, time::Duration}; use iroha_data_model::{prelude::*, transaction::TransactionLimits}; - use iroha_primitives::must_use::MustUse; use rand::Rng as _; use tokio::test; @@ -381,6 +404,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 +475,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 +496,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 +542,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")) @@ -524,7 +562,7 @@ pub mod tests { // Check that fully signed transaction passes signature check assert!(matches!( fully_signed_tx.check_signature_condition(&state_view), - MustUse(true) + true )); let get_tx = |key_pair| { @@ -534,10 +572,7 @@ pub mod tests { for key_pair in key_pairs { let partially_signed_tx: AcceptedTransaction = get_tx(key_pair); // Check that none of partially signed txs passes signature check - assert_eq!( - partially_signed_tx.check_signature_condition(&state_view), - MustUse(false) - ); + assert!(!partially_signed_tx.check_signature_condition(&state_view),); assert!(matches!( queue .push(partially_signed_tx, &state_view) @@ -560,7 +595,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 +625,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 +648,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 +674,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 +722,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 +757,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 +774,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 +821,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 +895,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 +956,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..daf7faae917 100644 --- a/core/src/smartcontracts/isi/query.rs +++ b/core/src/smartcontracts/isi/query.rs @@ -316,20 +316,24 @@ 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)?; + let _events = state_block.apply(&first_block)?; kura.store_block(first_block); for _ in 1u64..blocks { 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)?; + let _events = state_block.apply(&block)?; kura.store_block(block); } state_block.commit(); @@ -466,10 +470,12 @@ 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)?; + let _events = state_block.apply(&vcb)?; kura.store_block(vcb); state_block.commit(); 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..20255cdfe51 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}, @@ -16,7 +21,7 @@ use iroha_data_model::{ role::RoleId, }; use iroha_logger::prelude::*; -use iroha_primitives::{numeric::Numeric, small::SmallVec}; +use iroha_primitives::{must_use::MustUse, numeric::Numeric, small::SmallVec}; use parking_lot::Mutex; use range_bounds::RoleIdByAccountBounds; use serde::{ @@ -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() } @@ -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).into()) } /// Execute `block` transactions and store their hashes as well as @@ -1217,12 +1219,13 @@ 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<()> { + #[must_use] + 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 +1251,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(), + length: DEFAULT_CONSENSUS_ESTIMATION, } }); let interval = TimeInterval { since: block.as_ref().header().timestamp(), - length: block.as_ref().header().consensus_estimation(), + length: DEFAULT_CONSENSUS_ESTIMATION, }; TimeEvent { @@ -1388,7 +1411,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 +1774,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 +1783,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); @@ -1774,7 +1800,7 @@ mod tests { payload_mut(&mut block).header.previous_block_hash = block_hashes.last().copied(); block_hashes.push(block.as_ref().hash()); - state_block.apply(&block).unwrap(); + let _events = state_block.apply(&block).unwrap(); } assert!(state_block @@ -1788,7 +1814,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); @@ -1798,7 +1827,7 @@ mod tests { let mut block = block.clone(); payload_mut(&mut block).header.height = i as u64; - state_block.apply(&block).unwrap(); + let _events = state_block.apply(&block).unwrap(); kura.store_block(block); } @@ -1806,7 +1835,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..df7e925a220 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,18 @@ impl Sumeragi { info!( addr=%self.peer_id.address, role=%self.current_topology.role(&self.peer_id), - block_height=%state_block.height(), + block_height=%block.as_ref().header().height, block_hash=%block.as_ref().hash(), "{}", 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 +334,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 +342,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 +374,23 @@ impl Sumeragi { topology: &Topology, BlockCreated { block }: BlockCreated, ) -> Option> { - let block_hash = block.hash_of_payload(); + let block_hash = block.hash(); 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_hash, "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)) } @@ -434,30 +424,30 @@ impl Sumeragi { match (message, role) { (BlockMessage::BlockSyncUpdate(BlockSyncUpdate { block }), _) => { let block_hash = block.hash(); - info!(%addr, %role, hash=%block_hash, "Block sync update received"); + info!(%addr, %role, block=%block_hash, "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().hash(), 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_hash, ?error, "Block not valid.") } Err((_, BlockSyncError::SoftForkBlockNotValid(error))) => { - error!(%addr, %role, %block_hash, ?error, "Soft-fork block not valid.") + error!(%addr, %role, block=%block_hash, ?error, "Soft-fork block not valid.") } Err(( _, @@ -470,7 +460,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_hash, consensus_latest_block_view_change_index=%block_view_change_index, "Soft fork doesn't occurred: block has the same or smaller view change index" ); @@ -496,28 +486,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,20 +523,19 @@ 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_hash = v_block.block.as_ref().hash(); + 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_hash, "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(); @@ -554,10 +545,10 @@ impl Sumeragi { let block_hash = v_block.block.as_ref().hash(); 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_hash, "Block validated, signed and forwarded"); *voting_block = Some(v_block); } else { error!(%addr, %role, "Received BlockCreated message, but shouldn't"); @@ -641,33 +632,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().hash(), "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 +670,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().hash(), "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 +859,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 +921,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().hash(), "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 +994,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 +1073,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 +1093,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 +1128,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,10 +1200,14 @@ 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); - - let genesis = block.commit(topology).expect("Block is valid"); - state_block.apply(&genesis).expect("Failed to apply block"); + .sign(leader_key_pair) + .unpack(|_| {}); + + let genesis = block + .commit(topology) + .unpack(|_| {}) + .expect("Block is valid"); + let _events = state_block.apply(&genesis).expect("Failed to apply block"); state_block.commit(); kura.store_block(genesis); @@ -1256,6 +1246,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 +1267,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 +1283,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 _events = state_block.apply_without_execution(&committed_block); state_block.commit(); kura.store_block(committed_block); @@ -1305,7 +1298,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 +1317,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 +1341,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 +1357,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 _events = state_block.apply_without_execution(&committed_block); state_block.commit(); kura.store_block(committed_block); @@ -1378,7 +1373,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 +1392,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 _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 +1407,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 +1434,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/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/crypto/src/signature/mod.rs b/crypto/src/signature/mod.rs index b22eac891a7..66c75ef8e71 100644 --- a/crypto/src/signature/mod.rs +++ b/crypto/src/signature/mod.rs @@ -55,7 +55,7 @@ ffi::ffi_item! { public_key: PublicKey, /// Signature payload #[serde_as(as = "serde_with::hex::Hex")] - payload: ConstVec, + payload: ConstVec, } } 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..d98c1f6de1f 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>), +/// } /// } /// */ /// ``` @@ -73,7 +77,7 @@ pub fn enum_ref(input: TokenStream) -> Result { /// use iroha_data_model_derive::model; /// /// #[model] -/// pub mod model { +/// mod model { /// pub struct DataModel1 { /// pub item1: u32, /// item2: u64 @@ -86,7 +90,7 @@ pub fn enum_ref(input: TokenStream) -> Result { /// } /// /// /* will produce: -/// pub mod model { +/// mod model { /// pub struct DataModel1 { /// #[cfg(feature = "transparent_api")] /// pub item1: u32, diff --git a/data_model/derive/src/model.rs b/data_model/derive/src/model.rs index a5fdb7a7510..915368dd3c8 100644 --- a/data_model/derive/src/model.rs +++ b/data_model/derive/src/model.rs @@ -15,14 +15,16 @@ pub fn impl_model(emitter: &mut Emitter, input: &syn::ItemMod) -> TokenStream { .. } = input; - let syn::Visibility::Public(vis_public) = vis else { + if *vis != syn::Visibility::Inherited { emit!( emitter, input, - "The `model` attribute can only be used on public modules" + "The `model` attribute can only be used on private modules" ); + return quote!(); }; + if ident != "model" { emit!( emitter, @@ -38,7 +40,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..fa0cd7fdcd4 100644 --- a/data_model/src/account.rs +++ b/data_model/src/account.rs @@ -14,8 +14,6 @@ use derive_more::{Constructor, DebugCustom, Display}; use getset::Getters; use iroha_data_model_derive::{model, IdEqOrdHash}; use iroha_primitives::const_vec::ConstVec; -#[cfg(feature = "transparent_api")] -use iroha_primitives::must_use::MustUse; use iroha_schema::IntoSchema; use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; @@ -39,7 +37,7 @@ pub type AccountsMap = btree_map::BTreeMap; type Signatories = btree_set::BTreeSet; #[model] -pub mod model { +mod model { use super::*; /// Identification of an [`Account`]. Consists of Account name and Domain name. @@ -276,10 +274,11 @@ impl Account { /// Checks whether the transaction contains all the signatures required by the /// [`SignatureCheckCondition`] stored in this account. + #[must_use] pub fn check_signature_check_condition( &self, transaction_signatories: &btree_set::BTreeSet, - ) -> MustUse { + ) -> bool { self.signature_check_condition .check(&self.signatories, transaction_signatories) } @@ -399,12 +398,13 @@ impl SignatureCheckCondition { Self::AllAccountSignaturesAnd(ConstVec::new_empty()) } + #[must_use] #[cfg(feature = "transparent_api")] fn check( &self, account_signatories: &btree_set::BTreeSet, transaction_signatories: &btree_set::BTreeSet, - ) -> MustUse { + ) -> bool { let result = match &self { SignatureCheckCondition::AnyAccountSignatureOr(additional_allowed_signatures) => { account_signatories @@ -420,7 +420,7 @@ impl SignatureCheckCondition { } }; - MustUse::new(result) + result } } @@ -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}; @@ -452,7 +454,7 @@ mod tests { let tx_signatories = tx_signatories.iter().copied().cloned().collect(); assert_eq!( - condition.check(&account_signatories, &tx_signatories,).0, + condition.check(&account_signatories, &tx_signatories,), result ); } diff --git a/data_model/src/asset.rs b/data_model/src/asset.rs index 6bf4cf6447d..36e589a02ec 100644 --- a/data_model/src/asset.rs +++ b/data_model/src/asset.rs @@ -33,7 +33,7 @@ pub type AssetDefinitionsMap = btree_map::BTreeMap; #[model] -pub mod model { +mod model { use iroha_macro::FromVariant; use super::*; diff --git a/data_model/src/block.rs b/data_model/src/block.rs index 93ce5bec045..ab92e076a3f 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}; @@ -25,7 +24,9 @@ pub use self::model::*; use crate::{events::prelude::*, peer, transaction::prelude::*}; #[model] -pub mod model { +mod model { + use getset::{CopyGetters, Getters}; + use super::*; #[derive( @@ -37,6 +38,7 @@ pub mod model { PartialOrd, Ord, Getters, + CopyGetters, Decode, Encode, Deserialize, @@ -48,22 +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, /// Hash of the previous block in the chain. + #[getset(get = "pub")] pub previous_block_hash: Option>, /// Hash of merkle tree root of transactions' hashes. + #[getset(get = "pub")] pub transactions_hash: Option>>, + /// Creation timestamp (unix time in milliseconds). + #[getset(skip)] + pub timestamp_ms: u64, /// Value of view change index. Used to resolve soft forks. - pub view_change_index: u64, #[getset(skip)] + pub view_change_index: u64, /// Estimation of consensus duration (in milliseconds). pub consensus_estimation_ms: u64, } @@ -76,7 +80,6 @@ pub mod model { Eq, PartialOrd, Ord, - Getters, Decode, Encode, Deserialize, @@ -84,45 +87,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 +120,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] @@ -153,11 +132,6 @@ impl BlockHeader { 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) - } } impl SignedBlockV1 { @@ -168,21 +142,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 { @@ -213,8 +187,8 @@ impl SignedBlock { } /// Add additional signatures to this block - #[cfg(feature = "transparent_api")] #[must_use] + #[cfg(feature = "transparent_api")] pub fn sign(mut self, key_pair: &KeyPair) -> Self { let SignedBlock::V1(block) = &mut self; let signature = iroha_crypto::SignatureOf::new(key_pair, &block.payload); @@ -292,7 +266,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 @@ -357,7 +331,7 @@ pub mod stream { use super::*; #[model] - pub mod model { + mod model { use core::num::NonZeroU64; use super::*; @@ -392,7 +366,7 @@ pub mod error { use super::*; #[model] - pub mod model { + mod model { use super::*; /// The reason for rejecting a transaction with new blocks. diff --git a/data_model/src/domain.rs b/data_model/src/domain.rs index c631996224d..89df2d7bcfd 100644 --- a/data_model/src/domain.rs +++ b/data_model/src/domain.rs @@ -23,7 +23,7 @@ use crate::{ }; #[model] -pub mod model { +mod model { use super::*; /// Identification of a [`Domain`]. diff --git a/data_model/src/events/data/events.rs b/data_model/src/events/data/events.rs index 156be776be1..6ee1e5b8dde 100644 --- a/data_model/src/events/data/events.rs +++ b/data_model/src/events/data/events.rs @@ -36,7 +36,7 @@ macro_rules! data_event { // NOTE: if adding/editing events here, make sure to update the corresponding event filter in [`super::filter`] #[model] -pub mod model { +mod model { use super::*; use crate::metadata::MetadataValueBox; @@ -146,7 +146,7 @@ mod asset { } #[model] - pub mod model { + mod model { use super::*; /// Depending on the wrapping event, [`Self`] represents the added or removed asset quantity. @@ -259,7 +259,7 @@ mod role { } #[model] - pub mod model { + mod model { use super::*; /// Depending on the wrapping event, [`RolePermissionChanged`] role represents the added or removed role's permission @@ -296,7 +296,7 @@ mod permission { use crate::permission::PermissionTokenSchema; #[model] - pub mod model { + mod model { use super::*; /// Information about permission tokens update. @@ -364,7 +364,7 @@ mod account { } #[model] - pub mod model { + mod model { use super::*; /// Depending on the wrapping event, [`AccountPermissionChanged`] role represents the added or removed account role @@ -451,7 +451,7 @@ mod domain { } #[model] - pub mod model { + mod model { use super::*; /// Event indicate that owner of the [`Domain`] is changed @@ -499,7 +499,7 @@ mod trigger { } #[model] - pub mod model { + mod model { use super::*; /// Depending on the wrapping event, [`Self`] represents the increased or decreased number of event executions. @@ -548,7 +548,7 @@ mod executor { use super::*; #[model] - pub mod model { + mod model { use iroha_data_model_derive::EventSet; diff --git a/data_model/src/events/data/filters.rs b/data_model/src/events/data/filters.rs index 4edc08c828e..9c3078f3793 100644 --- a/data_model/src/events/data/filters.rs +++ b/data_model/src/events/data/filters.rs @@ -13,7 +13,7 @@ pub use self::model::*; use super::*; #[model] -pub mod model { +mod model { use super::*; #[derive( @@ -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/execute_trigger.rs b/data_model/src/events/execute_trigger.rs index eb672703b38..4a43f3b9f19 100644 --- a/data_model/src/events/execute_trigger.rs +++ b/data_model/src/events/execute_trigger.rs @@ -8,7 +8,7 @@ use super::*; use crate::prelude::*; #[model] -pub mod model { +mod model { use super::*; /// Trigger execution event. Produced every time the `ExecuteTrigger` instruction is executed. diff --git a/data_model/src/events/mod.rs b/data_model/src/events/mod.rs index 94c003526bc..d63c5ba80b8 100644 --- a/data_model/src/events/mod.rs +++ b/data_model/src/events/mod.rs @@ -7,6 +7,7 @@ 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::*; @@ -18,7 +19,7 @@ pub mod time; pub mod trigger_completed; #[model] -pub mod model { +mod model { use super::*; #[allow(missing_docs)] @@ -37,9 +38,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 +86,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 +117,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 +127,50 @@ 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 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 +201,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 +234,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, } @@ -210,7 +259,10 @@ impl EventFilter for TriggeringEventFilterBox { } mod conversions { - use super::prelude::*; + use super::{ + pipeline::{BlockEventFilter, TransactionEventFilter}, + prelude::*, + }; macro_rules! last_tt { ($last:tt) => { @@ -257,6 +309,12 @@ mod conversions { RoleEventFilter => DataEventFilter => TriggeringEventFilterBox, ConfigurationEventFilter => DataEventFilter => TriggeringEventFilterBox, ExecutorEventFilter => DataEventFilter => TriggeringEventFilterBox, + + TransactionEventFilter => PipelineEventFilterBox => TriggeringEventFilterBox, + BlockEventFilter => PipelineEventFilterBox => TriggeringEventFilterBox, + + TransactionEventFilter => PipelineEventFilterBox => EventFilterBox, + BlockEventFilter => PipelineEventFilterBox => EventFilterBox, } } @@ -272,23 +330,23 @@ pub mod stream { use super::*; #[model] - pub mod model { + mod model { use super::*; /// Message sent by the stream producer. /// 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 +361,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..2e6c8c08dae 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 { +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,148 +78,256 @@ 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, + pub struct TransactionEvent { + #[getset(get = "pub")] + pub hash: HashOf, + #[getset(get_copy = "pub")] + pub block_height: Option, + #[getset(get = "pub")] + pub status: TransactionStatus, } - /// [`Status`] of the entity. + /// Report of block's status in the pipeline #[derive( Debug, Clone, + Copy, PartialEq, 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, - ), + pub struct BlockEventFilter { + #[getset(get_copy = "pub")] + pub height: Option, + #[getset(get = "pub")] + pub status: Option, + } + + #[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Default, + Getters, + Decode, + Encode, + Deserialize, + Serialize, + IntoSchema, + )] + #[ffi_type] + pub struct TransactionEventFilter { + #[getset(get = "pub")] + pub hash: Option>, + #[getset(get_copy = "pub")] + pub block_height: Option>, + #[getset(get = "pub")] + pub status: Option, } } -impl PipelineEventFilter { - /// Creates a new [`PipelineEventFilter`] accepting all [`PipelineEvent`]s +impl BlockEventFilter { + /// Construct new instance #[must_use] - #[inline] pub const fn new() -> Self { Self { - status_kind: None, - entity_kind: None, - hash: None, + height: None, + status: None, } } - /// Modifies a [`PipelineEventFilter`] to accept only [`PipelineEvent`]s originating from a specific entity kind (block/transaction). + /// Match only block with the given height #[must_use] - #[inline] - pub const fn for_entity(mut self, entity_kind: PipelineEntityKind) -> Self { - self.entity_kind = Some(entity_kind); + pub fn for_height(mut self, height: u64) -> Self { + self.height = Some(height); self } - /// Modifies a [`PipelineEventFilter`] to accept only [`PipelineEvent`]s with a specific status. + /// Match only block with the given status #[must_use] - #[inline] - pub const fn for_status(mut self, status_kind: PipelineStatusKind) -> Self { - self.status_kind = Some(status_kind); + pub fn for_status(mut self, status: BlockStatus) -> Self { + self.status = Some(status); self } +} - /// Modifies a [`PipelineEventFilter`] to accept only [`PipelineEvent`]s originating from an entity with specified hash. +impl TransactionEventFilter { + /// Construct new instance #[must_use] - #[inline] - pub const fn for_hash(mut self, hash: Hash) -> Self { + pub const fn new() -> Self { + Self { + hash: None, + block_height: None, + status: None, + } + } + + /// Match only transactions with the given block height + #[must_use] + pub fn for_block_height(mut self, block_height: Option) -> Self { + self.block_height = Some(block_height); + self + } + + /// Match only transactions with the given hash + #[must_use] + 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 +337,123 @@ 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, + previous_block_hash: None, + transactions_hash: None, + timestamp_ms: 0, + view_change_index: 0, + consensus_estimation_ms: 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/events/time.rs b/data_model/src/events/time.rs index fad7e76a7d2..9548abb0088 100644 --- a/data_model/src/events/time.rs +++ b/data_model/src/events/time.rs @@ -9,7 +9,7 @@ pub use self::model::*; use super::*; #[model] -pub mod model { +mod model { use super::*; /// Special event that is emitted when state is ready for handling time-triggers diff --git a/data_model/src/events/trigger_completed.rs b/data_model/src/events/trigger_completed.rs index 9d0d43d195c..05c34842392 100644 --- a/data_model/src/events/trigger_completed.rs +++ b/data_model/src/events/trigger_completed.rs @@ -16,7 +16,7 @@ pub use self::model::*; use crate::trigger::TriggerId; #[model] -pub mod model { +mod model { use super::*; /// Event that notifies that a trigger was executed diff --git a/data_model/src/executor.rs b/data_model/src/executor.rs index 8207dcb6250..8c147cc477b 100644 --- a/data_model/src/executor.rs +++ b/data_model/src/executor.rs @@ -14,7 +14,7 @@ pub use self::model::*; use crate::transaction::WasmSmartContract; #[model] -pub mod model { +mod model { use super::*; /// executor that checks if an operation satisfies some conditions. diff --git a/data_model/src/ipfs.rs b/data_model/src/ipfs.rs index 13ff6cf0610..46f57cad0ad 100644 --- a/data_model/src/ipfs.rs +++ b/data_model/src/ipfs.rs @@ -15,7 +15,7 @@ pub use self::model::*; use crate::ParseError; #[model] -pub mod model { +mod model { use super::*; /// Represents path in IPFS. Performs checks to ensure path validity. diff --git a/data_model/src/isi.rs b/data_model/src/isi.rs index 1ecd901aba7..66a2b706e38 100644 --- a/data_model/src/isi.rs +++ b/data_model/src/isi.rs @@ -29,7 +29,7 @@ pub trait Instruction: Into + seal::Sealed { } #[model] -pub mod model { +mod model { use iroha_macro::FromVariant; pub use transparent::*; @@ -1271,7 +1271,7 @@ pub mod error { }; #[model] - pub mod model { + mod model { use serde::{Deserialize, Serialize}; use super::*; diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index 87352bead15..7112685653b 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -243,7 +243,7 @@ pub mod parameter { } #[model] - pub mod model { + mod model { use super::*; #[derive( @@ -616,8 +616,7 @@ pub mod parameter { } #[model] -#[allow(irrefutable_let_patterns)] // Triggered from derives macros -pub mod model { +mod model { use super::*; /// Unique id of blockchain @@ -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/metadata.rs b/data_model/src/metadata.rs index 871f14d9c51..110a13fd2cb 100644 --- a/data_model/src/metadata.rs +++ b/data_model/src/metadata.rs @@ -29,7 +29,7 @@ pub type Path = [Name]; pub type UnlimitedMetadata = btree_map::BTreeMap; #[model] -pub mod model { +mod model { use super::*; /// Collection of parameters by their names with checked insertion. diff --git a/data_model/src/name.rs b/data_model/src/name.rs index 9de18a5a7c3..e4370fac05b 100644 --- a/data_model/src/name.rs +++ b/data_model/src/name.rs @@ -15,7 +15,7 @@ pub use self::model::*; use crate::{isi::error::InvalidParameterError, ParseError}; #[model] -pub mod model { +mod model { use super::*; /// `Name` struct represents the type of Iroha Entities names, such as diff --git a/data_model/src/peer.rs b/data_model/src/peer.rs index 695d61823f0..a158aa87f63 100644 --- a/data_model/src/peer.rs +++ b/data_model/src/peer.rs @@ -19,7 +19,7 @@ pub use self::model::*; use crate::{Identifiable, PublicKey, Registered}; #[model] -pub mod model { +mod model { use getset::Getters; use super::*; diff --git a/data_model/src/permission.rs b/data_model/src/permission.rs index a8ccc715d31..840afc30b0f 100644 --- a/data_model/src/permission.rs +++ b/data_model/src/permission.rs @@ -21,7 +21,7 @@ use super::*; pub type PermissionTokenId = Name; #[model] -pub mod model { +mod model { use super::*; /// Stored proof of the account having a permission for a certain action. diff --git a/data_model/src/query/cursor.rs b/data_model/src/query/cursor.rs index e18f707f3ad..3deaba04750 100644 --- a/data_model/src/query/cursor.rs +++ b/data_model/src/query/cursor.rs @@ -22,7 +22,7 @@ const QUERY_ID: &str = "query_id"; const CURSOR: &str = "cursor"; #[model] -pub mod model { +mod model { use super::*; /// Forward-only (a.k.a non-scrollable) cursor diff --git a/data_model/src/query/mod.rs b/data_model/src/query/mod.rs index 7fdb0b9a149..922dce74cfe 100644 --- a/data_model/src/query/mod.rs +++ b/data_model/src/query/mod.rs @@ -85,7 +85,7 @@ macro_rules! queries { pub use self::model::*; #[iroha_data_model_derive::model] - pub mod model{ + mod model{ use super::*; $( #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -114,7 +114,7 @@ pub trait Query: Into + seal::Sealed { } #[model] -pub mod model { +mod model { use getset::Getters; use iroha_crypto::HashOf; use iroha_macro::FromVariant; @@ -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) @@ -1160,7 +1160,7 @@ pub mod http { declare_versioned_with_scale!(SignedQuery 1..2, Debug, Clone, iroha_macro::FromVariant, IntoSchema); #[model] - pub mod model { + mod model { use core::num::NonZeroU64; use super::*; @@ -1347,7 +1347,7 @@ pub mod error { use crate::{executor, permission, prelude::*}; #[model] - pub mod model { + mod model { use super::*; /// Query errors. diff --git a/data_model/src/query/sorting.rs b/data_model/src/query/sorting.rs index d6bd3a1230f..460c803d214 100644 --- a/data_model/src/query/sorting.rs +++ b/data_model/src/query/sorting.rs @@ -18,7 +18,7 @@ use crate::{name::Name, prelude::*}; const SORT_BY_KEY: &str = "sort_by_metadata_key"; #[model] -pub mod model { +mod model { use super::*; /// Struct for sorting requests diff --git a/data_model/src/role.rs b/data_model/src/role.rs index eedf399030c..49cd4618d5d 100644 --- a/data_model/src/role.rs +++ b/data_model/src/role.rs @@ -17,7 +17,7 @@ use crate::{ }; #[model] -pub mod model { +mod model { use super::*; /// Identification of a role. 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..ed29345b458 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; @@ -27,7 +26,9 @@ use crate::{ }; #[model] -pub mod model { +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, + /// Creation timestamp (unix time in milliseconds). + pub creation_time_ms: u64, /// 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. @@ -448,7 +440,9 @@ pub mod error { use super::*; #[model] - pub mod model { + 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, } } @@ -620,7 +612,7 @@ mod http { use super::*; #[model] - pub mod model { + mod model { use super::*; /// Structure that represents the initial state of a transaction before the transaction receives any signatures. @@ -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..16c2a2803cb 100644 --- a/data_model/src/trigger.rs +++ b/data_model/src/trigger.rs @@ -23,7 +23,7 @@ use crate::{ }; #[model] -pub mod model { +mod model { use super::*; /// Identification of a `Trigger`. @@ -133,7 +133,7 @@ pub mod action { use crate::account::AccountId; #[model] - pub mod model { + mod model { use super::*; /// Designed to differentiate between oneshot and unlimited @@ -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 496bb8eec28..5e5c102ebce 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -594,14 +594,38 @@ } ] }, - "BlockHeader": { + "BlockEvent": { + "Struct": [ + { + "name": "header", + "type": "BlockHeader" + }, + { + "name": "hash", + "type": "HashOf" + }, + { + "name": "status", + "type": "BlockStatus" + } + ] + }, + "BlockEventFilter": { "Struct": [ { "name": "height", - "type": "u64" + "type": "Option" }, { - "name": "timestamp_ms", + "name": "status", + "type": "Option" + } + ] + }, + "BlockHeader": { + "Struct": [ + { + "name": "height", "type": "u64" }, { @@ -612,6 +636,10 @@ "name": "transactions_hash", "type": "Option>>" }, + { + "name": "timestamp_ms", + "type": "u64" + }, { "name": "view_change_index", "type": "u64" @@ -639,7 +667,7 @@ }, { "name": "event_recommendations", - "type": "Vec" + "type": "Vec" } ] }, @@ -651,6 +679,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": [ @@ -1023,12 +1072,12 @@ "u32" ] }, - "Event": { + "EventBox": { "Enum": [ { "tag": "Pipeline", "discriminant": 0, - "type": "PipelineEvent" + "type": "PipelineEventBox" }, { "tag": "Data", @@ -1057,7 +1106,7 @@ { "tag": "Pipeline", "discriminant": 0, - "type": "PipelineEventFilter" + "type": "PipelineEventFilterBox" }, { "tag": "Data", @@ -1081,8 +1130,8 @@ } ] }, - "EventMessage": "Event", - "EventSubscriptionRequest": "EventFilterBox", + "EventMessage": "EventBox", + "EventSubscriptionRequest": "Vec", "Executable": { "Enum": [ { @@ -2405,21 +2454,24 @@ "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" }, @@ -2429,18 +2481,15 @@ "Option>": { "Option": "NonZero" }, + "Option>": { + "Option": "Option" + }, "Option": { "Option": "ParameterId" }, "Option": { "Option": "PeerId" }, - "Option": { - "Option": "PipelineEntityKind" - }, - "Option": { - "Option": "PipelineStatusKind" - }, "Option": { "Option": "RoleId" }, @@ -2453,6 +2502,9 @@ "Option": { "Option": "TransactionRejectionReason" }, + "Option": { + "Option": "TransactionStatus" + }, "Option": { "Option": "TriggerCompletedOutcomeType" }, @@ -2462,6 +2514,9 @@ "Option": { "Option": "u32" }, + "Option": { + "Option": "u64" + }, "Parameter": { "Struct": [ { @@ -2603,94 +2658,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" } ] }, @@ -3786,6 +3778,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": [ { @@ -3812,14 +3836,14 @@ "name": "chain_id", "type": "ChainId" }, - { - "name": "creation_time_ms", - "type": "u64" - }, { "name": "authority", "type": "AccountId" }, + { + "name": "creation_time_ms", + "type": "u64" + }, { "name": "instructions", "type": "Executable" @@ -3876,10 +3900,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" } ] }, @@ -4127,7 +4168,7 @@ { "tag": "Pipeline", "discriminant": 0, - "type": "PipelineEventFilter" + "type": "PipelineEventFilterBox" }, { "tag": "Data", @@ -4295,8 +4336,11 @@ } ] }, - "Vec": { - "Vec": "Event" + "Vec": { + "Vec": "EventBox" + }, + "Vec": { + "Vec": "EventFilterBox" }, "Vec>": { "Vec": "GenericPredicateBox" diff --git a/primitives/src/must_use.rs b/primitives/src/must_use.rs index 40457a2009f..3b6823178d3 100644 --- a/primitives/src/must_use.rs +++ b/primitives/src/must_use.rs @@ -4,7 +4,7 @@ use core::borrow::{Borrow, BorrowMut}; use derive_more::{AsMut, AsRef, Constructor, Deref, Display}; -/// Wrapper type to annotate types with `must_use` attribute +/// Wrapper type to annotate types with `must_use` attribute. Only to be used with [`Result`] /// /// # Example /// ``` @@ -30,19 +30,7 @@ use derive_more::{AsMut, AsRef, Constructor, Deref, Display}; /// // is_odd(3).unwrap(); /// ``` #[derive( - Constructor, - Debug, - Display, - Copy, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - AsRef, - AsMut, - Deref, + Debug, Display, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Constructor, AsRef, AsMut, Deref, )] #[repr(transparent)] #[must_use] @@ -56,6 +44,12 @@ impl MustUse { } } +impl From for MustUse { + fn from(source: T) -> Self { + MustUse(source) + } +} + impl Borrow for MustUse { #[inline] fn borrow(&self) -> &T { diff --git a/schema/gen/src/lib.rs b/schema/gen/src/lib.rs index ebfb956f436..874dc171663 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,27 @@ 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 +271,8 @@ types!( PermissionToken, PermissionTokenSchema, PermissionTokenSchemaUpdateEvent, - PipelineEntityKind, - PipelineEvent, - PipelineEventFilter, - PipelineRejectionReason, - PipelineStatus, - PipelineStatusKind, + PipelineEventBox, + PipelineEventFilterBox, PredicateBox, PublicKey, QueryBox, @@ -338,12 +340,15 @@ types!( TimeEventFilter, TimeInterval, TimeSchedule, + TransactionEvent, + TransactionEventFilter, TransactionLimitError, TransactionLimits, TransactionPayload, TransactionQueryOutput, TransactionRejectionReason, TransactionValue, + TransactionStatus, Transfer, Transfer, Transfer, @@ -372,7 +377,8 @@ types!( UnregisterBox, Upgrade, ValidationFail, - Vec, + Vec, + Vec, Vec, Vec, Vec, @@ -412,6 +418,7 @@ mod tests { BlockHeader, BlockPayload, SignedBlock, SignedBlockV1, }, domain::NewDomain, + events::pipeline::{BlockEventFilter, TransactionEventFilter}, executor::Executor, ipfs::IpfsPath, isi::{ @@ -435,7 +442,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/derive/src/lib.rs b/telemetry/derive/src/lib.rs index 471260e9ee3..21f1d5bcbaf 100644 --- a/telemetry/derive/src/lib.rs +++ b/telemetry/derive/src/lib.rs @@ -242,11 +242,16 @@ fn impl_metrics(emitter: &mut Emitter, _specs: &MetricSpecs, func: &syn::ItemFn) 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 diff --git a/telemetry/src/dev.rs b/telemetry/src/dev.rs index 43055a72339..1ca1511f3a0 100644 --- a/telemetry/src/dev.rs +++ b/telemetry/src/dev.rs @@ -63,7 +63,7 @@ pub async fn start_file_output( async fn write_telemetry(file: &mut File, item: &FuturePollTelemetry) -> Result<()> { let mut json = serde_json::to_string(&item).wrap_err("failed to serialize telemetry to JSON")?; - json.push_str("\n"); + json.push('\n'); file.write_all(json.as_bytes()) .await .wrap_err("failed to write data to the file")?; 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(()); }