diff --git a/core/src/smartcontracts/isi/mod.rs b/core/src/smartcontracts/isi/mod.rs index 4bc587aabee..2a81e8abb9c 100644 --- a/core/src/smartcontracts/isi/mod.rs +++ b/core/src/smartcontracts/isi/mod.rs @@ -21,6 +21,7 @@ use storage::storage::StorageReadOnly; use super::Execute; use crate::{ prelude::*, + smartcontracts::triggers::set::SetReadOnly, state::{StateReadOnly, StateTransaction, WorldReadOnly}, }; diff --git a/core/src/smartcontracts/isi/triggers/mod.rs b/core/src/smartcontracts/isi/triggers/mod.rs index 00ea4338215..48714e27450 100644 --- a/core/src/smartcontracts/isi/triggers/mod.rs +++ b/core/src/smartcontracts/isi/triggers/mod.rs @@ -96,7 +96,7 @@ pub mod isi { let trigger_id = self.object_id.clone(); let triggers = &mut state_transaction.world.triggers; - if triggers.remove(&trigger_id) { + if triggers.remove(trigger_id.clone()) { state_transaction .world .emit_events(Some(TriggerEvent::Deleted(self.object_id))); @@ -299,7 +299,7 @@ pub mod query { }; use super::*; - use crate::{prelude::*, state::StateReadOnly}; + use crate::{prelude::*, smartcontracts::triggers::set::SetReadOnly, state::StateReadOnly}; impl ValidQuery for FindAllActiveTriggerIds { #[metrics(+"find_all_active_triggers")] @@ -307,7 +307,7 @@ pub mod query { &self, state_ro: &'state impl StateReadOnly, ) -> Result + 'state>, Error> { - Ok(Box::new(state_ro.world().triggers().ids().cloned())) + Ok(Box::new(state_ro.world().triggers().ids_iter().cloned())) } } diff --git a/core/src/smartcontracts/isi/triggers/set.rs b/core/src/smartcontracts/isi/triggers/set.rs index 71dd449e711..28c19de6b67 100644 --- a/core/src/smartcontracts/isi/triggers/set.rs +++ b/core/src/smartcontracts/isi/triggers/set.rs @@ -11,7 +11,7 @@ use core::cmp::min; use std::{fmt, num::NonZeroU64}; -use indexmap::{map::Entry, IndexMap}; +use indexmap::IndexMap; use iroha_crypto::HashOf; use iroha_data_model::{ events::EventFilter, @@ -25,6 +25,13 @@ use serde::{ ser::{SerializeMap, SerializeStruct}, Serialize, Serializer, }; +use storage::{ + cell::{Block as CellBlock, Cell, Transaction as CellTransaction, View as CellView}, + storage::{ + Block as StorageBlock, Storage, StorageReadOnly, Transaction as StorageTransaction, + View as StorageView, + }, +}; use thiserror::Error; use crate::{ @@ -49,59 +56,135 @@ pub type Result = core::result::Result; /// [`WasmSmartContract`]s by [`TriggerId`]. /// Stored together with number to count triggers with identical [`WasmSmartContract`]. -type WasmSmartContractMap = IndexMap, WasmSmartContractEntry>; +type WasmSmartContractMap = Storage, WasmSmartContractEntry>; +type WasmSmartContractMapBlock<'set> = + StorageBlock<'set, HashOf, WasmSmartContractEntry>; +type WasmSmartContractMapTransaction<'block, 'set> = + StorageTransaction<'block, 'set, HashOf, WasmSmartContractEntry>; +type WasmSmartContractMapView<'set> = + StorageView<'set, HashOf, WasmSmartContractEntry>; /// Specialized structure that maps event filters to Triggers. // NB: `Set` has custom `Serialize` and `DeserializeSeed` implementations // which need to be manually updated when changing the struct -#[derive(Debug, Default)] +#[derive(Default)] pub struct Set { /// Triggers using [`DataEventFilter`] - data_triggers: IndexMap>, + data_triggers: Storage>, /// Triggers using [`PipelineEventFilterBox`] - pipeline_triggers: IndexMap>, + pipeline_triggers: Storage>, /// Triggers using [`TimeEventFilter`] - time_triggers: IndexMap>, + time_triggers: Storage>, /// Triggers using [`ExecuteTriggerEventFilter`] - by_call_triggers: IndexMap>, + by_call_triggers: Storage>, /// Trigger ids with type of events they process - ids: IndexMap, + ids: Storage, /// [`WasmSmartContract`]s map by hash for querying and optimization purposes. 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<(EventBox, TriggerId)>, + // NOTE: Cell is used because matched_ids changed as whole (not granularly) + matched_ids: Cell>, +} + +/// Trigger set for block's aggregated changes +pub struct SetBlock<'set> { + /// Triggers using [`DataEventFilter`] + data_triggers: StorageBlock<'set, TriggerId, LoadedAction>, + /// Triggers using [`PipelineEventFilterBox`] + pipeline_triggers: StorageBlock<'set, TriggerId, LoadedAction>, + /// Triggers using [`TimeEventFilter`] + time_triggers: StorageBlock<'set, TriggerId, LoadedAction>, + /// Triggers using [`ExecuteTriggerEventFilter`] + by_call_triggers: StorageBlock<'set, TriggerId, LoadedAction>, + /// Trigger ids with type of events they process + ids: StorageBlock<'set, TriggerId, TriggeringEventType>, + /// Original [`WasmSmartContract`]s by [`TriggerId`] for querying purposes. + contracts: WasmSmartContractMapBlock<'set>, + /// List of actions that should be triggered by events provided by `handle_*` methods. + /// Vector is used to save the exact triggers order. + matched_ids: CellBlock<'set, Vec<(EventBox, TriggerId)>>, +} + +/// Trigger set for transaction's aggregated changes +pub struct SetTransaction<'block, 'set> { + /// Triggers using [`DataEventFilter`] + data_triggers: StorageTransaction<'block, 'set, TriggerId, LoadedAction>, + /// Triggers using [`PipelineEventFilterBox`] + pipeline_triggers: + StorageTransaction<'block, 'set, TriggerId, LoadedAction>, + /// Triggers using [`TimeEventFilter`] + time_triggers: StorageTransaction<'block, 'set, TriggerId, LoadedAction>, + /// Triggers using [`ExecuteTriggerEventFilter`] + by_call_triggers: + StorageTransaction<'block, 'set, TriggerId, LoadedAction>, + /// Trigger ids with type of events they process + ids: StorageTransaction<'block, 'set, TriggerId, TriggeringEventType>, + /// Original [`WasmSmartContract`]s by [`TriggerId`] for querying purposes. + contracts: WasmSmartContractMapTransaction<'block, 'set>, + /// List of actions that should be triggered by events provided by `handle_*` methods. + /// Vector is used to save the exact triggers order. + matched_ids: CellTransaction<'block, 'set, Vec<(EventBox, TriggerId)>>, +} + +/// Consistent point in time view of the [`Set`] +pub struct SetView<'set> { + /// Triggers using [`DataEventFilter`] + data_triggers: StorageView<'set, TriggerId, LoadedAction>, + /// Triggers using [`PipelineEventFilterBox`] + pipeline_triggers: StorageView<'set, TriggerId, LoadedAction>, + /// Triggers using [`TimeEventFilter`] + time_triggers: StorageView<'set, TriggerId, LoadedAction>, + /// Triggers using [`ExecuteTriggerEventFilter`] + by_call_triggers: StorageView<'set, TriggerId, LoadedAction>, + /// Trigger ids with type of events they process + ids: StorageView<'set, TriggerId, TriggeringEventType>, + /// Original [`WasmSmartContract`]s by [`TriggerId`] for querying purposes. + contracts: WasmSmartContractMapView<'set>, + /// List of actions that should be triggered by events provided by `handle_*` methods. + /// Vector is used to save the exact triggers order. + matched_ids: CellView<'set, Vec<(EventBox, TriggerId)>>, } +/// Entry in wasm smart-contracts map #[derive(Debug, Clone)] -struct WasmSmartContractEntry { +pub struct WasmSmartContractEntry { + /// Original wasm binary blob original_contract: WasmSmartContract, + /// Compiled with [`wasmtime`] smart-contract compiled_contract: wasmtime::Module, + /// Number of times this contract is used count: NonZeroU64, } /// Helper struct for serializing triggers. -struct TriggersWithContext<'s, F> { +struct TriggersWithContext<'s, F> +where + F: storage::Value, +{ /// Triggers being serialized - triggers: &'s IndexMap>, + triggers: &'s StorageView<'s, TriggerId, LoadedAction>, /// Containing Set, used for looking up original [`WasmSmartContract`]s /// during serialization. - set: &'s Set, + set: &'s SetView<'s>, } -impl<'s, F> TriggersWithContext<'s, F> { - fn new(triggers: &'s IndexMap>, set: &'s Set) -> Self { +impl<'s, F: storage::Value> TriggersWithContext<'s, F> { + fn new( + triggers: &'s StorageView<'s, TriggerId, LoadedAction>, + set: &'s SetView<'s>, + ) -> Self { Self { triggers, set } } } -impl Serialize for TriggersWithContext<'_, F> { +impl Serialize for TriggersWithContext<'_, F> { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut map = serializer.serialize_map(Some(self.triggers.len()))?; - for (id, action) in self.triggers { + for (id, action) in self.triggers.iter() { let action = self.set.get_original_action(action.clone()); map.serialize_entry(&id, &action)?; } @@ -114,33 +197,25 @@ impl Serialize for Set { where S: serde::Serializer, { - let &Self { - data_triggers, - pipeline_triggers, - time_triggers, - by_call_triggers, - ids, - contracts: _contracts, - matched_ids: _matched_ids, - } = &self; + let set_view = self.view(); let mut set = serializer.serialize_struct("Set", 6)?; set.serialize_field( "data_triggers", - &TriggersWithContext::new(data_triggers, self), + &TriggersWithContext::new(&set_view.data_triggers, &set_view), )?; set.serialize_field( "pipeline_triggers", - &TriggersWithContext::new(pipeline_triggers, self), + &TriggersWithContext::new(&set_view.pipeline_triggers, &set_view), )?; set.serialize_field( "time_triggers", - &TriggersWithContext::new(time_triggers, self), + &TriggersWithContext::new(&set_view.time_triggers, &set_view), )?; set.serialize_field( "by_call_triggers", - &TriggersWithContext::new(by_call_triggers, self), + &TriggersWithContext::new(&set_view.by_call_triggers, &set_view), )?; - set.serialize_field("ids", ids)?; + set.serialize_field("ids", &self.ids)?; set.end() } } @@ -167,18 +242,21 @@ impl<'de> DeserializeSeed<'de> for WasmSeed<'_, Set> { where M: MapAccess<'de>, { - let mut set = Set::default(); + let set = Set::default(); + let mut set_block = set.block(); + let mut set_transaction = set_block.transaction(); while let Some(key) = map.next_key::()? { match key.as_str() { "data_triggers" => { let triggers: IndexMap> = map.next_value()?; for (id, action) in triggers { - set.add_data_trigger( - self.loader.engine, - SpecializedTrigger::new(id, action), - ) - .unwrap(); + set_transaction + .add_data_trigger( + self.loader.engine, + SpecializedTrigger::new(id, action), + ) + .unwrap(); } } "pipeline_triggers" => { @@ -187,22 +265,24 @@ impl<'de> DeserializeSeed<'de> for WasmSeed<'_, Set> { SpecializedAction, > = map.next_value()?; for (id, action) in triggers { - set.add_pipeline_trigger( - self.loader.engine, - SpecializedTrigger::new(id, action), - ) - .unwrap(); + set_transaction + .add_pipeline_trigger( + self.loader.engine, + SpecializedTrigger::new(id, action), + ) + .unwrap(); } } "time_triggers" => { let triggers: IndexMap> = map.next_value()?; for (id, action) in triggers { - set.add_time_trigger( - self.loader.engine, - SpecializedTrigger::new(id, action), - ) - .unwrap(); + set_transaction + .add_time_trigger( + self.loader.engine, + SpecializedTrigger::new(id, action), + ) + .unwrap(); } } "by_call_triggers" => { @@ -211,19 +291,26 @@ impl<'de> DeserializeSeed<'de> for WasmSeed<'_, Set> { SpecializedAction, > = map.next_value()?; for (id, action) in triggers { - set.add_by_call_trigger( - self.loader.engine, - SpecializedTrigger::new(id, action), - ) - .unwrap(); + set_transaction + .add_by_call_trigger( + self.loader.engine, + SpecializedTrigger::new(id, action), + ) + .unwrap(); } } + // TODO: ids look redundant because we insert ids already through `add_` methods "ids" => { - set.ids = map.next_value()?; + let ids: IndexMap = map.next_value()?; + for (id, event_type) in ids { + set_transaction.ids.insert(id, event_type); + } } _ => { /* Ignore unknown fields */ } } } + set_transaction.apply(); + set_block.commit(); Ok(set) } @@ -233,190 +320,38 @@ impl<'de> DeserializeSeed<'de> for WasmSeed<'_, Set> { } } -impl Clone for Set { - fn clone(&self) -> Self { - Self { - data_triggers: self.data_triggers.clone(), - pipeline_triggers: self.pipeline_triggers.clone(), - time_triggers: self.time_triggers.clone(), - by_call_triggers: self.by_call_triggers.clone(), - ids: self.ids.clone(), - contracts: self.contracts.clone(), - matched_ids: Vec::default(), - } - } -} - -impl Set { - /// Add trigger with [`DataEventFilter`] - /// - /// Return `false` if a trigger with given id already exists - /// - /// # Errors - /// - /// Return [`Err`] if failed to preload wasm trigger - #[inline] - pub fn add_data_trigger( - &mut self, - engine: &wasmtime::Engine, - trigger: SpecializedTrigger, - ) -> Result { - self.add_to(engine, trigger, TriggeringEventType::Data, |me| { - &mut me.data_triggers - }) - } - - /// Add trigger with [`PipelineEventFilterBox`] - /// - /// Return `false` if a trigger with given id already exists - /// - /// # Errors - /// - /// Return [`Err`] if failed to preload wasm trigger - #[inline] - pub fn add_pipeline_trigger( - &mut self, - engine: &wasmtime::Engine, - trigger: SpecializedTrigger, - ) -> Result { - self.add_to(engine, trigger, TriggeringEventType::Pipeline, |me| { - &mut me.pipeline_triggers - }) - } - - /// Add trigger with [`TimeEventFilter`] - /// - /// Returns `false` if a trigger with given id already exists - /// - /// # Errors - /// - /// Return [`Err`] if failed to preload wasm trigger - #[inline] - pub fn add_time_trigger( - &mut self, - engine: &wasmtime::Engine, - trigger: SpecializedTrigger, - ) -> Result { - self.add_to(engine, trigger, TriggeringEventType::Time, |me| { - &mut me.time_triggers - }) - } - - /// Add trigger with [`ExecuteTriggerEventFilter`] - /// - /// Returns `false` if a trigger with given id already exists - /// - /// # Errors - /// - /// Return [`Err`] if failed to preload wasm trigger - #[inline] - pub fn add_by_call_trigger( - &mut self, - engine: &wasmtime::Engine, - trigger: SpecializedTrigger, - ) -> Result { - self.add_to(engine, trigger, TriggeringEventType::ExecuteTrigger, |me| { - &mut me.by_call_triggers - }) - } - - /// Add generic trigger to generic collection - /// - /// Returns `false` if a trigger with given id already exists - /// - /// # Errors - /// - /// Return [`Err`] if failed to preload wasm trigger - fn add_to( - &mut self, - engine: &wasmtime::Engine, - trigger: SpecializedTrigger, - event_type: TriggeringEventType, - map: impl FnOnce(&mut Self) -> &mut IndexMap>, - ) -> Result { - let SpecializedTrigger { - id: trigger_id, - action: - SpecializedAction { - executable, - repeats, - authority, - filter, - metadata, - }, - } = trigger; - - if self.contains(&trigger_id) { - return Ok(false); - } - - let loaded_executable = match executable { - Executable::Wasm(bytes) => { - let hash = HashOf::new(&bytes); - // Store original executable representation to respond to queries with. - let module = match self.contracts.entry(hash) { - Entry::Occupied(mut occupied) => { - let WasmSmartContractEntry { - compiled_contract, - count, - .. - } = occupied.get_mut(); - // Considering 1 trigger registration takes 1 second, - // it would take 584 942 417 355 years to overflow. - *count = count.checked_add(1).expect( - "There is no way someone could register 2^64 amount of same triggers", - ); - // Cloning module is cheap, under Arc inside - compiled_contract.clone() - } - Entry::Vacant(vacant) => { - let module = wasm::load_module(engine, &bytes)?; - // Cloning module is cheap, under Arc inside - vacant.insert(WasmSmartContractEntry { - original_contract: bytes, - compiled_contract: module.clone(), - count: NonZeroU64::MIN, - }); - module - } - }; - LoadedExecutable::Wasm(LoadedWasm { - module, - blob_hash: hash, - }) - } - Executable::Instructions(instructions) => LoadedExecutable::Instructions(instructions), - }; - map(self).insert( - trigger_id.clone(), - LoadedAction { - executable: loaded_executable, - repeats, - authority, - filter, - metadata, - }, - ); - self.ids.insert(trigger_id, event_type); - Ok(true) - } +/// Trait to perform read-only operations on [`WorldBlock`], [`WorldTransaction`] and [`WorldView`] +#[allow(missing_docs)] +pub trait SetReadOnly { + fn data_triggers(&self) -> &impl StorageReadOnly>; + fn pipeline_triggers( + &self, + ) -> &impl StorageReadOnly>; + fn time_triggers(&self) -> &impl StorageReadOnly>; + fn by_call_triggers( + &self, + ) -> &impl StorageReadOnly>; + fn ids(&self) -> &impl StorageReadOnly; + fn contracts(&self) + -> &impl StorageReadOnly, WasmSmartContractEntry>; + fn matched_ids(&self) -> &[(EventBox, TriggerId)]; /// Get original [`WasmSmartContract`] for [`TriggerId`]. /// Returns `None` if there's no [`Trigger`] /// with specified `id` that has WASM executable #[inline] - pub fn get_original_contract( + fn get_original_contract( &self, hash: &HashOf, ) -> Option<&WasmSmartContract> { - self.contracts + self.contracts() .get(hash) .map(|entry| &entry.original_contract) } /// Convert [`LoadedAction`] to original [`Action`] by retrieving original /// [`WasmSmartContract`] if applicable - pub fn get_original_action(&self, action: LoadedAction) -> SpecializedAction { + fn get_original_action(&self, action: LoadedAction) -> SpecializedAction { let LoadedAction { executable, repeats, @@ -447,40 +382,40 @@ impl Set { /// Get all contained trigger ids without a particular order #[inline] - pub fn ids(&self) -> impl ExactSizeIterator { - self.ids.keys() + fn ids_iter(&self) -> impl Iterator { + self.ids().iter().map(|(trigger_id, _)| trigger_id) } /// Get [`LoadedExecutable`] for given [`TriggerId`]. /// Returns `None` if `id` is not in the set. - pub fn get_executable(&self, id: &TriggerId) -> Option<&LoadedExecutable> { - let event_type = self.ids.get(id)?; + fn get_executable(&self, id: &TriggerId) -> Option<&LoadedExecutable> { + let event_type = self.ids().get(id)?; Some(match event_type { TriggeringEventType::Data => { &self - .data_triggers + .data_triggers() .get(id) .expect("`Set::data_triggers` doesn't contain required id. This is a bug") .executable } TriggeringEventType::Pipeline => { &self - .pipeline_triggers + .pipeline_triggers() .get(id) .expect("`Set::pipeline_triggers` doesn't contain required id. This is a bug") .executable } TriggeringEventType::Time => { &self - .time_triggers + .time_triggers() .get(id) .expect("`Set::time_triggers` doesn't contain required id. This is a bug") .executable } TriggeringEventType::ExecuteTrigger => { &self - .by_call_triggers + .by_call_triggers() .get(id) .expect("`Set::by_call_triggers` doesn't contain required id. This is a bug") .executable @@ -491,17 +426,17 @@ impl Set { /// Apply `f` to triggers that belong to the given [`DomainId`] /// /// Return an empty list if [`Set`] doesn't contain any triggers belonging to [`DomainId`]. - pub fn inspect_by_domain_id<'a, F, R>( + fn inspect_by_domain_id<'a, F: 'a, R>( &'a self, domain_id: &DomainId, f: F, ) -> impl Iterator + '_ where - F: Fn(&TriggerId, &dyn LoadedActionTrait) -> R + 'a, + F: Fn(&TriggerId, &dyn LoadedActionTrait) -> R, { let domain_id = domain_id.clone(); - self.ids.iter().filter_map(move |(id, event_type)| { + self.ids().iter().filter_map(move |(id, event_type)| { let trigger_domain_id = id.domain_id.as_ref()?; if *trigger_domain_id != domain_id { @@ -510,22 +445,22 @@ impl Set { let result = match event_type { TriggeringEventType::Data => self - .data_triggers + .data_triggers() .get(id) .map(|trigger| f(id, trigger)) .expect("`Set::data_triggers` doesn't contain required id. This is a bug"), TriggeringEventType::Pipeline => self - .pipeline_triggers + .pipeline_triggers() .get(id) .map(|trigger| f(id, trigger)) .expect("`Set::pipeline_triggers` doesn't contain required id. This is a bug"), TriggeringEventType::Time => self - .time_triggers + .time_triggers() .get(id) .map(|trigger| f(id, trigger)) .expect("`Set::time_triggers` doesn't contain required id. This is a bug"), TriggeringEventType::ExecuteTrigger => self - .by_call_triggers + .by_call_triggers() .get(id) .map(|trigger| f(id, trigger)) .expect("`Set::by_call_triggers` doesn't contain required id. This is a bug"), @@ -538,36 +473,331 @@ impl Set { /// Apply `f` to the trigger identified by `id`. /// /// Return [`None`] if [`Set`] doesn't contain the trigger with the given `id`. - pub fn inspect_by_id(&self, id: &TriggerId, f: F) -> Option + fn inspect_by_id(&self, id: &TriggerId, f: F) -> Option where F: Fn(&dyn LoadedActionTrait) -> R, { - let event_type = self.ids.get(id).copied()?; + let event_type = self.ids().get(id).copied()?; let result = match event_type { TriggeringEventType::Data => self - .data_triggers + .data_triggers() .get(id) .map(|entry| f(entry)) .expect("`Set::data_triggers` doesn't contain required id. This is a bug"), TriggeringEventType::Pipeline => self - .pipeline_triggers + .pipeline_triggers() .get(id) .map(|entry| f(entry)) .expect("`Set::pipeline_triggers` doesn't contain required id. This is a bug"), TriggeringEventType::Time => self - .time_triggers + .time_triggers() .get(id) .map(|entry| f(entry)) .expect("`Set::time_triggers` doesn't contain required id. This is a bug"), TriggeringEventType::ExecuteTrigger => self - .by_call_triggers + .by_call_triggers() .get(id) .map(|entry| f(entry)) .expect("`Set::by_call_triggers` doesn't contain required id. This is a bug"), }; Some(result) } +} + +macro_rules! impl_set_ro { + ($($ident:ty),*) => {$( + impl SetReadOnly for $ident { + fn data_triggers(&self) -> &impl StorageReadOnly> { + &self.data_triggers + } + fn pipeline_triggers(&self) -> &impl StorageReadOnly> { + &self.pipeline_triggers + } + fn time_triggers(&self) -> &impl StorageReadOnly> { + &self.time_triggers + } + fn by_call_triggers(&self) -> &impl StorageReadOnly> { + &self.by_call_triggers + } + fn ids(&self) -> &impl StorageReadOnly { + &self.ids + } + fn contracts(&self) -> &impl StorageReadOnly, WasmSmartContractEntry> { + &self.contracts + } + fn matched_ids(&self) -> &[(EventBox, TriggerId)] { + &self.matched_ids + } + } + )*}; +} + +impl_set_ro! { + SetBlock<'_>, SetTransaction<'_, '_>, SetView<'_> +} + +impl Set { + /// Create struct to apply block's changes + pub fn block(&self) -> SetBlock<'_> { + SetBlock { + data_triggers: self.data_triggers.block(), + pipeline_triggers: self.pipeline_triggers.block(), + time_triggers: self.time_triggers.block(), + by_call_triggers: self.by_call_triggers.block(), + ids: self.ids.block(), + contracts: self.contracts.block(), + matched_ids: self.matched_ids.block(), + } + } + + /// Create struct to apply block's changes while reverting changes made in the latest block + pub fn block_and_revert(&self) -> SetBlock<'_> { + SetBlock { + data_triggers: self.data_triggers.block_and_revert(), + pipeline_triggers: self.pipeline_triggers.block_and_revert(), + time_triggers: self.time_triggers.block_and_revert(), + by_call_triggers: self.by_call_triggers.block_and_revert(), + ids: self.ids.block_and_revert(), + contracts: self.contracts.block_and_revert(), + matched_ids: self.matched_ids.block_and_revert(), + } + } + + /// Create point in time view of the [`World`] + pub fn view(&self) -> SetView<'_> { + SetView { + data_triggers: self.data_triggers.view(), + pipeline_triggers: self.pipeline_triggers.view(), + time_triggers: self.time_triggers.view(), + by_call_triggers: self.by_call_triggers.view(), + ids: self.ids.view(), + contracts: self.contracts.view(), + matched_ids: self.matched_ids.view(), + } + } +} + +impl<'set> SetBlock<'set> { + /// Create struct to apply transaction's changes + pub fn transaction(&mut self) -> SetTransaction<'_, 'set> { + SetTransaction { + data_triggers: self.data_triggers.transaction(), + pipeline_triggers: self.pipeline_triggers.transaction(), + time_triggers: self.time_triggers.transaction(), + by_call_triggers: self.by_call_triggers.transaction(), + ids: self.ids.transaction(), + contracts: self.contracts.transaction(), + matched_ids: self.matched_ids.transaction(), + } + } + + /// Commit block's changes + pub fn commit(self) { + // NOTE: commit in reverse order + self.matched_ids.commit(); + self.contracts.commit(); + self.ids.commit(); + self.by_call_triggers.commit(); + self.time_triggers.commit(); + self.pipeline_triggers.commit(); + self.data_triggers.commit(); + } + + /// Handle [`TimeEvent`]. + /// + /// Find all actions that are triggered by `event` and store them. + /// These actions are inspected in the next [`Set::inspect_matched()`] call. + pub fn handle_time_event(&mut self, event: TimeEvent) { + for (id, action) in self.time_triggers.iter() { + let mut count = action.filter.count_matches(&event); + if let Repeats::Exactly(repeats) = action.repeats { + count = min(repeats, count); + } + if count == 0 { + continue; + } + + let ids = core::iter::repeat_with(|| (EventBox::Time(event), id.clone())).take( + count + .try_into() + .expect("`u32` should always fit in `usize`"), + ); + self.matched_ids.extend(ids); + } + } + + /// Extract `matched_id` + pub fn extract_matched_ids(&mut self) -> Vec<(EventBox, TriggerId)> { + core::mem::take(&mut self.matched_ids) + } +} + +impl<'block, 'set> SetTransaction<'block, 'set> { + /// Apply transaction's changes + pub fn apply(self) { + // NOTE: apply in reverse order + self.matched_ids.apply(); + self.contracts.apply(); + self.ids.apply(); + self.by_call_triggers.apply(); + self.time_triggers.apply(); + self.pipeline_triggers.apply(); + self.data_triggers.apply(); + } + + /// Add trigger with [`DataEventFilter`] + /// + /// Return `false` if a trigger with given id already exists + /// + /// # Errors + /// + /// Return [`Err`] if failed to preload wasm trigger + #[inline] + pub fn add_data_trigger( + &mut self, + engine: &wasmtime::Engine, + trigger: SpecializedTrigger, + ) -> Result { + self.add_to(engine, trigger, TriggeringEventType::Data, |me| { + &mut me.data_triggers + }) + } + + /// Add trigger with [`PipelineEventFilterBox`] + /// + /// Return `false` if a trigger with given id already exists + /// + /// # Errors + /// + /// Return [`Err`] if failed to preload wasm trigger + #[inline] + pub fn add_pipeline_trigger( + &mut self, + engine: &wasmtime::Engine, + trigger: SpecializedTrigger, + ) -> Result { + self.add_to(engine, trigger, TriggeringEventType::Pipeline, |me| { + &mut me.pipeline_triggers + }) + } + + /// Add trigger with [`TimeEventFilter`] + /// + /// Returns `false` if a trigger with given id already exists + /// + /// # Errors + /// + /// Return [`Err`] if failed to preload wasm trigger + #[inline] + pub fn add_time_trigger( + &mut self, + engine: &wasmtime::Engine, + trigger: SpecializedTrigger, + ) -> Result { + self.add_to(engine, trigger, TriggeringEventType::Time, |me| { + &mut me.time_triggers + }) + } + + /// Add trigger with [`ExecuteTriggerEventFilter`] + /// + /// Returns `false` if a trigger with given id already exists + /// + /// # Errors + /// + /// Return [`Err`] if failed to preload wasm trigger + #[inline] + pub fn add_by_call_trigger( + &mut self, + engine: &wasmtime::Engine, + trigger: SpecializedTrigger, + ) -> Result { + self.add_to(engine, trigger, TriggeringEventType::ExecuteTrigger, |me| { + &mut me.by_call_triggers + }) + } + + /// Add generic trigger to generic collection + /// + /// Returns `false` if a trigger with given id already exists + /// + /// # Errors + /// + /// Return [`Err`] if failed to preload wasm trigger + fn add_to( + &mut self, + engine: &wasmtime::Engine, + trigger: SpecializedTrigger, + event_type: TriggeringEventType, + map: impl FnOnce(&mut Self) -> &mut StorageTransaction<'block, 'set, TriggerId, LoadedAction>, + ) -> Result { + let SpecializedTrigger { + id: trigger_id, + action: + SpecializedAction { + executable, + repeats, + authority, + filter, + metadata, + }, + } = trigger; + + if self.ids.get(&trigger_id).is_some() { + return Ok(false); + } + + let loaded_executable = match executable { + Executable::Wasm(bytes) => { + let hash = HashOf::new(&bytes); + // Store original executable representation to respond to queries with. + let module = if let Some(WasmSmartContractEntry { + compiled_contract, + count, + .. + }) = self.contracts.get_mut(&hash) + { + // Considering 1 trigger registration takes 1 second, + // it would take 584 942 417 355 years to overflow. + *count = count.checked_add(1).expect( + "There is no way someone could register 2^64 amount of same triggers", + ); + // Cloning module is cheap, under Arc inside + compiled_contract.clone() + } else { + let module = wasm::load_module(engine, &bytes)?; + // Cloning module is cheap, under Arc inside + self.contracts.insert( + hash, + WasmSmartContractEntry { + original_contract: bytes, + compiled_contract: module.clone(), + count: NonZeroU64::MIN, + }, + ); + module + }; + LoadedExecutable::Wasm(LoadedWasm { + module, + blob_hash: hash, + }) + } + Executable::Instructions(instructions) => LoadedExecutable::Instructions(instructions), + }; + map(self).insert( + trigger_id.clone(), + LoadedAction { + executable: loaded_executable, + repeats, + authority, + filter, + metadata, + }, + ); + self.ids.insert(trigger_id, event_type); + Ok(true) + } /// Apply `f` to the trigger identified by `id`. /// @@ -610,8 +840,8 @@ impl Set { /// # Panics /// /// Panics on inconsistent state of [`Set`]. This is a bug. - pub fn remove(&mut self, id: &TriggerId) -> bool { - let Some(event_type) = self.ids.remove(id) else { + pub fn remove(&mut self, id: TriggerId) -> bool { + let Some(event_type) = self.ids.remove(id.clone()) else { return false; }; @@ -638,21 +868,46 @@ impl Set { true } + /// Modify repetitions of the hook identified by [`Id`]. + /// + /// # Errors + /// + /// - If a trigger with the given id is not found. + /// - If updating the current trigger `repeats` causes an overflow. Indefinitely + /// repeating triggers and triggers set for exact time always cause an overflow. + pub fn mod_repeats( + &mut self, + id: &TriggerId, + f: impl Fn(u32) -> Result, + ) -> Result<(), ModRepeatsError> { + self.inspect_by_id_mut(id, |action| match action.repeats() { + Repeats::Exactly(repeats) => { + let new_repeats = f(*repeats)?; + action.set_repeats(Repeats::Exactly(new_repeats)); + Ok(()) + } + _ => Err(ModRepeatsError::RepeatsOverflow(RepeatsOverflowError)), + }) + .ok_or_else(|| ModRepeatsError::NotFound(id.clone())) + // .flatten() -- unstable + .and_then(std::convert::identity) + } + /// Remove trigger from `triggers` and decrease the counter of the original [`WasmSmartContract`]. /// /// Note that this function doesn't remove the trigger from [`Set::ids`]. /// /// Returns `true` if trigger was removed and `false` otherwise. - fn remove_from( - original_contracts: &mut WasmSmartContractMap, - triggers: &mut IndexMap>, - trigger_id: &TriggerId, + fn remove_from( + contracts: &mut WasmSmartContractMapTransaction<'block, 'set>, + triggers: &mut StorageTransaction<'block, 'set, TriggerId, LoadedAction>, + trigger_id: TriggerId, ) -> bool { triggers .remove(trigger_id) .map(|loaded_action| { if let Some(blob_hash) = loaded_action.extract_blob_hash() { - Self::remove_original_trigger(original_contracts, blob_hash); + Self::remove_original_trigger(contracts, blob_hash); } }) .is_some() @@ -663,56 +918,70 @@ impl Set { /// /// # Panics /// - /// Panics if `blob_hash` is not in the [`Set::original_contracts`]. + /// Panics if `blob_hash` is not in the [`Set::contracts`]. fn remove_original_trigger( - original_contracts: &mut WasmSmartContractMap, + contracts: &mut WasmSmartContractMapTransaction, blob_hash: HashOf, ) { #[allow(clippy::option_if_let_else)] // More readable this way - match original_contracts.entry(blob_hash) { - Entry::Occupied(mut entry) => { - let count = &mut entry.get_mut().count; + match contracts.get_mut(&blob_hash) { + Some(entry) => { + let count = &mut entry.count; if let Some(new_count) = NonZeroU64::new(count.get() - 1) { *count = new_count; } else { - entry.remove(); + contracts.remove(blob_hash); } } - Entry::Vacant(_) => { - panic!("`Set::original_contracts` doesn't contain required hash. This is a bug") + None => { + panic!("`Set::contracts` doesn't contain required hash. This is a bug") } } } - /// Check if [`Set`] contains `id`. - #[inline] - pub fn contains(&self, id: &TriggerId) -> bool { - self.ids.contains_key(id) + /// Decrease `action`s for provided triggers and remove those whose counter reached zero. + pub fn decrease_repeats(&mut self, triggers: &[TriggerId]) { + for id in triggers { + // Ignoring error if trigger has not `Repeats::Exact(_)` but something else + let _mod_repeats_res = self.mod_repeats(id, |n| Ok(n.saturating_sub(1))); + } + + let Self { + data_triggers, + pipeline_triggers, + time_triggers, + by_call_triggers, + ids, + contracts, + .. + } = self; + Self::remove_zeros(ids, contracts, data_triggers); + Self::remove_zeros(ids, contracts, pipeline_triggers); + Self::remove_zeros(ids, contracts, time_triggers); + Self::remove_zeros(ids, contracts, by_call_triggers); } - /// Modify repetitions of the hook identified by [`Id`]. - /// - /// # Errors - /// - /// - If a trigger with the given id is not found. - /// - If updating the current trigger `repeats` causes an overflow. Indefinitely - /// repeating triggers and triggers set for exact time always cause an overflow. - pub fn mod_repeats( - &mut self, - id: &TriggerId, - f: impl Fn(u32) -> Result, - ) -> Result<(), ModRepeatsError> { - self.inspect_by_id_mut(id, |action| match action.repeats() { - Repeats::Exactly(repeats) => { - let new_repeats = f(*repeats)?; - action.set_repeats(Repeats::Exactly(new_repeats)); - Ok(()) + /// Remove actions with zero execution count from `triggers` + fn remove_zeros( + ids: &mut StorageTransaction<'block, 'set, TriggerId, TriggeringEventType>, + contracts: &mut WasmSmartContractMapTransaction<'block, 'set>, + triggers: &mut StorageTransaction<'block, 'set, TriggerId, LoadedAction>, + ) { + let to_remove: Vec = triggers + .iter() + .filter_map(|(id, action)| { + if let Repeats::Exactly(0) = action.repeats { + return Some(id.clone()); } - _ => Err(ModRepeatsError::RepeatsOverflow(RepeatsOverflowError)), + None }) - .ok_or_else(|| ModRepeatsError::NotFound(id.clone())) - // .flatten() -- unstable - .and_then(std::convert::identity) + .collect(); + + for id in to_remove { + ids.remove(id.clone()) + .and_then(|_| Self::remove_from(contracts, triggers, id).then_some(())) + .expect("`Set`'s `ids`, `contracts` and typed trigger collections are inconsistent. This is a bug") + } } /// Handle [`DataEvent`]. @@ -741,29 +1010,6 @@ impl Set { }; } - /// Handle [`TimeEvent`]. - /// - /// Find all actions that are triggered by `event` and store them. - /// These actions are inspected in the next [`Set::inspect_matched()`] call. - pub fn handle_time_event(&mut self, event: TimeEvent) { - for (id, action) in &self.time_triggers { - let mut count = action.filter.count_matches(&event); - if let Repeats::Exactly(repeats) = action.repeats { - count = min(repeats, count); - } - if count == 0 { - continue; - } - - let ids = core::iter::repeat_with(|| (EventBox::Time(event), id.clone())).take( - count - .try_into() - .expect("`u32` should always fit in `usize`"), - ); - self.matched_ids.extend(ids); - } - } - /// Match and insert a [`TriggerId`] into the set of matched ids. /// /// Skips insertion: @@ -786,56 +1032,6 @@ impl Set { matched_ids.push((event.into(), id.clone())); } - - /// Decrease `action`s for provided triggers and remove those whose counter reached zero. - pub fn decrease_repeats(&mut self, triggers: &[TriggerId]) { - for id in triggers { - // Ignoring error if trigger has not `Repeats::Exact(_)` but something else - let _mod_repeats_res = self.mod_repeats(id, |n| Ok(n.saturating_sub(1))); - } - - let Self { - data_triggers, - pipeline_triggers, - time_triggers, - by_call_triggers, - ids, - contracts: original_contracts, - .. - } = self; - Self::remove_zeros(ids, original_contracts, data_triggers); - Self::remove_zeros(ids, original_contracts, pipeline_triggers); - Self::remove_zeros(ids, original_contracts, time_triggers); - Self::remove_zeros(ids, original_contracts, by_call_triggers); - } - - /// Remove actions with zero execution count from `triggers` - fn remove_zeros( - ids: &mut IndexMap, - original_contracts: &mut WasmSmartContractMap, - triggers: &mut IndexMap>, - ) { - let to_remove: Vec = triggers - .iter() - .filter_map(|(id, action)| { - if let Repeats::Exactly(0) = action.repeats { - return Some(id.clone()); - } - None - }) - .collect(); - - for id in to_remove { - ids.remove(&id) - .and_then(|_| Self::remove_from(original_contracts, triggers, &id).then_some(())) - .expect("`Set`'s `ids`, `original_contracts` and typed trigger collections are inconsistent. This is a bug") - } - } - - /// Extract `matched_id` - pub fn extract_matched_ids(&mut self) -> Vec<(EventBox, TriggerId)> { - core::mem::take(&mut self.matched_ids) - } } /// WASM blob loaded with `wasmtime` diff --git a/core/src/smartcontracts/isi/world.rs b/core/src/smartcontracts/isi/world.rs index 30af4f45561..937bf96449b 100644 --- a/core/src/smartcontracts/isi/world.rs +++ b/core/src/smartcontracts/isi/world.rs @@ -127,7 +127,7 @@ pub mod isi { let world = &mut state_transaction.world; for trigger_id in &triggers_in_domain { - assert!(world.triggers.remove(trigger_id)); + assert!(world.triggers.remove(trigger_id.clone())); } if world.domains.remove(domain_id.clone()).is_none() { return Err(FindError::Domain(domain_id).into()); diff --git a/core/src/state.rs b/core/src/state.rs index 81ec0c79dec..881d60651af 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -45,7 +45,11 @@ use crate::{ smartcontracts::{ triggers::{ self, - set::{LoadedWasm, Set as TriggerSet}, + set::{ + LoadedWasm, Set as TriggerSet, SetBlock as TriggerSetBlock, + SetReadOnly as TriggerSetReadOnly, SetTransaction as TriggerSetTransaction, + SetView as TriggerSetView, + }, specialized::LoadedActionTrait, }, wasm, Execute, @@ -73,8 +77,7 @@ pub struct World { /// Registered permission token ids. pub(crate) permission_token_schema: Cell, /// Triggers - // TODO: refactor `TriggerSet` to use storage inside - pub(crate) triggers: Cell, + pub(crate) triggers: TriggerSet, /// Runtime Executor pub(crate) executor: Cell, } @@ -96,7 +99,7 @@ pub struct WorldBlock<'world> { /// Registered permission token ids. pub(crate) permission_token_schema: CellBlock<'world, PermissionTokenSchema>, /// Triggers - pub(crate) triggers: CellBlock<'world, TriggerSet>, + pub(crate) triggers: TriggerSetBlock<'world>, /// Runtime Executor pub(crate) executor: CellBlock<'world, Executor>, /// Events produced during execution of block @@ -121,7 +124,7 @@ pub struct WorldTransaction<'block, 'world> { /// Registered permission token ids. pub(crate) permission_token_schema: CellTransaction<'block, 'world, PermissionTokenSchema>, /// Triggers - pub(crate) triggers: CellTransaction<'block, 'world, TriggerSet>, + pub(crate) triggers: TriggerSetTransaction<'block, 'world>, /// Runtime Executor pub(crate) executor: CellTransaction<'block, 'world, Executor>, /// Events produced during execution of a transaction @@ -153,7 +156,7 @@ pub struct WorldView<'world> { /// Registered permission token ids. pub(crate) permission_token_schema: CellView<'world, PermissionTokenSchema>, /// Triggers - pub(crate) triggers: CellView<'world, TriggerSet>, + pub(crate) triggers: TriggerSetView<'world>, /// Runtime Executor pub(crate) executor: CellView<'world, Executor>, } @@ -332,7 +335,7 @@ pub trait WorldReadOnly { fn account_permission_tokens(&self) -> &impl StorageReadOnly; fn account_roles(&self) -> &impl StorageReadOnly; fn permission_token_schema(&self) -> &PermissionTokenSchema; - fn triggers(&self) -> &TriggerSet; + fn triggers(&self) -> &impl TriggerSetReadOnly; fn executor(&self) -> &Executor; // Domain-related methods @@ -580,7 +583,7 @@ macro_rules! impl_world_ro { fn permission_token_schema(&self) -> &PermissionTokenSchema { &self.permission_token_schema } - fn triggers(&self) -> &TriggerSet { + fn triggers(&self) -> &impl TriggerSetReadOnly { &self.triggers } fn executor(&self) -> &Executor { @@ -882,7 +885,7 @@ impl WorldTransaction<'_, '_> { /// /// Usable when you can't call [`Self::emit_events()`] due to mutable reference to self. fn emit_events_impl, T: Into>( - triggers: &mut TriggerSet, + triggers: &mut TriggerSetTransaction, events_buffer: &mut TransactionEventBuffer<'_>, world_events: I, ) { @@ -1339,7 +1342,9 @@ impl<'state> StateBlock<'state> { } } - self.world.triggers.decrease_repeats(&succeed); + let mut transaction = self.transaction(); + transaction.world.triggers.decrease_repeats(&succeed); + transaction.apply(); errors.is_empty().then_some(()).ok_or(errors) } @@ -1623,9 +1628,8 @@ pub(crate) mod deserialize { permission_token_schema = Some(map.next_value()?); } "triggers" => { - triggers = Some(map.next_value_seed(CellSeeded { - seed: self.loader.cast::(), - })?); + triggers = + Some(map.next_value_seed(self.loader.cast::())?); } "executor" => { executor = Some(map.next_value_seed(CellSeeded {