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 e5cf1a30c5a..b39baaca180 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 @@ -14,7 +14,7 @@ use lol_alloc::{FreeListAllocator, LockedAllocator}; static ALLOC: LockedAllocator = LockedAllocator::new(FreeListAllocator::new()); #[iroha_trigger::main] -fn main(_owner: AccountId, _event: Event) { +fn main(_id: TriggerId, _owner: AccountId, _event: Event) { iroha_trigger::log::info!("Executing trigger"); let accounts = FindAllAccounts.execute().dbg_unwrap(); let limits = MetadataLimits::new(256, 256); 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 7dd2d5c7c0d..e4dae2303e9 100644 --- a/client/tests/integration/smartcontracts/mint_rose_trigger/src/lib.rs +++ b/client/tests/integration/smartcontracts/mint_rose_trigger/src/lib.rs @@ -15,12 +15,17 @@ static ALLOC: LockedAllocator = LockedAllocator::new(FreeList /// Mint 1 rose for owner #[iroha_trigger::main] -fn main(owner: AccountId, _event: Event) { +fn main(id: TriggerId, owner: AccountId, _event: Event) { 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); - MintExpr::new(1_u32, rose_id) + let val: Value = FindTriggerKeyValueByIdAndKey::new(id, Name::from_str("VAL").unwrap()) + .execute() + .dbg_unwrap() + .into(); + + MintExpr::new(val, rose_id) .execute() .dbg_expect("Failed to mint rose"); } diff --git a/client/tests/integration/triggers/by_call_trigger.rs b/client/tests/integration/triggers/by_call_trigger.rs index 406d127a3ea..5b4bbed48a7 100644 --- a/client/tests/integration/triggers/by_call_trigger.rs +++ b/client/tests/integration/triggers/by_call_trigger.rs @@ -377,6 +377,13 @@ fn trigger_in_genesis_using_base64() -> Result<()> { let prev_value = get_asset_value(&mut test_client, asset_id.clone())?; // Executing trigger + test_client + .submit_blocking(SetKeyValueExpr::new( + trigger_id.clone(), + Name::from_str("VAL")?, + Value::from(1_u32), + )) + .unwrap(); let call_trigger = ExecuteTriggerExpr::new(trigger_id); test_client.submit_blocking(call_trigger)?; diff --git a/config/iroha_test_config.json b/config/iroha_test_config.json index 3c55e840892..fac3a4a2bb7 100644 --- a/config/iroha_test_config.json +++ b/config/iroha_test_config.json @@ -93,6 +93,10 @@ "max_len": 1048576, "max_entry_byte_size": 4096 }, + "TRIGGER_METADATA_LIMITS": { + "max_len": 1048576, + "max_entry_byte_size": 4096 + }, "IDENT_LENGTH_LIMITS": { "min": 1, "max": 128 diff --git a/config/src/wsv.rs b/config/src/wsv.rs index aacc58734be..7fbb1893928 100644 --- a/config/src/wsv.rs +++ b/config/src/wsv.rs @@ -38,6 +38,8 @@ pub struct Configuration { pub account_metadata_limits: MetadataLimits, /// [`MetadataLimits`] of any domain metadata. pub domain_metadata_limits: MetadataLimits, + /// [`MetadataLimits`] of any trigger metadata. + pub trigger_metadata_limits: MetadataLimits, /// [`LengthLimits`] for the number of chars in identifiers that can be stored in the WSV. pub ident_length_limits: LengthLimits, /// Limits that all transactions need to obey, in terms of size @@ -55,6 +57,7 @@ impl Default for ConfigurationProxy { asset_definition_metadata_limits: Some(DEFAULT_METADATA_LIMITS), account_metadata_limits: Some(DEFAULT_METADATA_LIMITS), domain_metadata_limits: Some(DEFAULT_METADATA_LIMITS), + trigger_metadata_limits: Some(DEFAULT_METADATA_LIMITS), ident_length_limits: Some(DEFAULT_IDENT_LENGTH_LIMITS), transaction_limits: Some(DEFAULT_TRANSACTION_LIMITS), wasm_runtime_config: Some(wasm::ConfigurationProxy::default()), @@ -75,12 +78,13 @@ pub mod tests { asset_definition_metadata_limits in prop::option::of(Just(DEFAULT_METADATA_LIMITS)), account_metadata_limits in prop::option::of(Just(DEFAULT_METADATA_LIMITS)), domain_metadata_limits in prop::option::of(Just(DEFAULT_METADATA_LIMITS)), + trigger_metadata_limits in prop::option::of(Just(DEFAULT_METADATA_LIMITS)), ident_length_limits in prop::option::of(Just(DEFAULT_IDENT_LENGTH_LIMITS)), transaction_limits in prop::option::of(Just(DEFAULT_TRANSACTION_LIMITS)), wasm_runtime_config in prop::option::of(Just(wasm::ConfigurationProxy::default())), ) -> ConfigurationProxy { - ConfigurationProxy { asset_metadata_limits, asset_definition_metadata_limits, account_metadata_limits, domain_metadata_limits, ident_length_limits, transaction_limits, wasm_runtime_config } + ConfigurationProxy { asset_metadata_limits, asset_definition_metadata_limits, account_metadata_limits, domain_metadata_limits, trigger_metadata_limits, ident_length_limits, transaction_limits, wasm_runtime_config } } } } diff --git a/configs/peer/config.json b/configs/peer/config.json index ef36a9f525c..0658faa8d9c 100644 --- a/configs/peer/config.json +++ b/configs/peer/config.json @@ -67,6 +67,10 @@ "max_len": 1048576, "max_entry_byte_size": 4096 }, + "TRIGGER_METADATA_LIMITS": { + "max_len": 1048576, + "max_entry_byte_size": 4096 + }, "IDENT_LENGTH_LIMITS": { "min": 1, "max": 128 diff --git a/configs/peer/executor.wasm b/configs/peer/executor.wasm index 294cd2ef7f9..d568d43a9e4 100644 Binary files a/configs/peer/executor.wasm and b/configs/peer/executor.wasm differ diff --git a/configs/peer/genesis.json b/configs/peer/genesis.json index 2ca5d0365ed..43b62446ac8 100644 --- a/configs/peer/genesis.json +++ b/configs/peer/genesis.json @@ -157,6 +157,11 @@ "Parameter": "?WSVDomainMetadataLimits=1048576,4096_ML" } }, + { + "NewParameter": { + "Parameter": "?WSVTriggerMetadataLimits=1048576,4096_ML" + } + }, { "NewParameter": { "Parameter": "?WSVIdentLengthLimits=1,128_LL" diff --git a/configs/peer/stable/config.json b/configs/peer/stable/config.json index ef36a9f525c..0658faa8d9c 100644 --- a/configs/peer/stable/config.json +++ b/configs/peer/stable/config.json @@ -67,6 +67,10 @@ "max_len": 1048576, "max_entry_byte_size": 4096 }, + "TRIGGER_METADATA_LIMITS": { + "max_len": 1048576, + "max_entry_byte_size": 4096 + }, "IDENT_LENGTH_LIMITS": { "min": 1, "max": 128 diff --git a/configs/peer/stable/genesis.json b/configs/peer/stable/genesis.json index 2ca5d0365ed..43b62446ac8 100644 --- a/configs/peer/stable/genesis.json +++ b/configs/peer/stable/genesis.json @@ -157,6 +157,11 @@ "Parameter": "?WSVDomainMetadataLimits=1048576,4096_ML" } }, + { + "NewParameter": { + "Parameter": "?WSVTriggerMetadataLimits=1048576,4096_ML" + } + }, { "NewParameter": { "Parameter": "?WSVIdentLengthLimits=1,128_LL" diff --git a/core/src/smartcontracts/isi/mod.rs b/core/src/smartcontracts/isi/mod.rs index ea009e6d575..859e32084bf 100644 --- a/core/src/smartcontracts/isi/mod.rs +++ b/core/src/smartcontracts/isi/mod.rs @@ -310,6 +310,12 @@ impl Execute for SetKeyValueExpr { value, } .execute(authority, wsv), + IdBox::TriggerId(object_id) => SetKeyValue::> { + object_id, + key, + value, + } + .execute(authority, wsv), _ => Err(Error::Evaluate(InstructionType::SetKeyValue.into())), } } @@ -332,6 +338,10 @@ impl Execute for RemoveKeyValueExpr { IdBox::DomainId(object_id) => { RemoveKeyValue:: { object_id, key }.execute(authority, wsv) } + IdBox::TriggerId(object_id) => { + RemoveKeyValue::> { object_id, key } + .execute(authority, wsv) + } _ => Err(Error::Evaluate(InstructionType::RemoveKeyValue.into())), } } diff --git a/core/src/smartcontracts/isi/triggers/mod.rs b/core/src/smartcontracts/isi/triggers/mod.rs index 7c814b6fe47..0d50c39c356 100644 --- a/core/src/smartcontracts/isi/triggers/mod.rs +++ b/core/src/smartcontracts/isi/triggers/mod.rs @@ -154,6 +154,59 @@ pub mod isi { } } + impl Execute for SetKeyValue> { + #[metrics(+"set_trigger_key_value")] + fn execute(self, _authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { + let trigger_id = self.object_id; + + let trigger_metadata_limits = wsv.config.account_metadata_limits; + wsv.world + .triggers + .inspect_by_id_mut(&trigger_id, |action| { + action.metadata_mut().insert_with_limits( + self.key.clone(), + self.value.clone(), + trigger_metadata_limits, + ) + }) + .ok_or(FindError::Trigger(trigger_id.clone()))??; + + wsv.emit_events(Some(TriggerEvent::MetadataInserted(MetadataChanged { + target_id: trigger_id, + key: self.key, + value: Box::new(self.value), + }))); + + Ok(()) + } + } + + impl Execute for RemoveKeyValue> { + #[metrics(+"remove_trigger_key_value")] + fn execute(self, _authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { + let trigger_id = self.object_id; + + let value = wsv + .world + .triggers + .inspect_by_id_mut(&trigger_id, |action| { + action + .metadata_mut() + .remove(&self.key) + .ok_or_else(|| FindError::MetadataKey(self.key.clone())) + }) + .ok_or(FindError::Trigger(trigger_id.clone()))??; + + wsv.emit_events(Some(TriggerEvent::MetadataRemoved(MetadataChanged { + target_id: trigger_id, + key: self.key, + value: Box::new(value), + }))); + + Ok(()) + } + } + impl Execute for ExecuteTriggerExpr { #[metrics(+"execute_trigger")] fn execute(self, authority: &AccountId, wsv: &mut WorldStateView) -> Result<(), Error> { diff --git a/core/src/smartcontracts/isi/triggers/set.rs b/core/src/smartcontracts/isi/triggers/set.rs index 57c1942e480..fb6deff9e04 100644 --- a/core/src/smartcontracts/isi/triggers/set.rs +++ b/core/src/smartcontracts/isi/triggers/set.rs @@ -84,6 +84,9 @@ pub trait LoadedActionTrait { /// Get action metadata fn metadata(&self) -> &Metadata; + /// Get action metadata + fn metadata_mut(&mut self) -> &mut Metadata; + /// Check if action is mintable. fn mintable(&self) -> bool; @@ -115,6 +118,10 @@ impl + Clone> LoadedActionTrait for Loaded &self.metadata } + fn metadata_mut(&mut self) -> &mut Metadata { + &mut self.metadata + } + fn mintable(&self) -> bool { self.filter.mintable() } diff --git a/core/src/smartcontracts/wasm.rs b/core/src/smartcontracts/wasm.rs index 03a9742aaa1..0717a156c01 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -428,6 +428,8 @@ pub mod state { /// Trigger execution state pub struct Trigger<'wrld> { pub(super) common: Common<'wrld>, + /// Id of this trigger + pub(in super::super) id: TriggerId, /// Event which activated this trigger pub(super) triggering_event: Event, } @@ -923,6 +925,7 @@ impl<'wrld> Runtime> { let span = wasm_log_span!("Trigger execution", %id, %authority); let state = state::Trigger { common: state::Common::new(wsv, authority, self.config, span), + id: id.clone(), triggering_event: event, }; @@ -941,6 +944,7 @@ impl<'wrld> Runtime> { #[codec::wrap] fn get_trigger_payload(state: &state::Trigger) -> payloads::Trigger { payloads::Trigger { + id: state.id.clone(), owner: state.authority().clone(), event: state.triggering_event.clone(), } diff --git a/data_model/src/events/data/events.rs b/data_model/src/events/data/events.rs index 8bf07a8e49b..fe2eb66bb03 100644 --- a/data_model/src/events/data/events.rs +++ b/data_model/src/events/data/events.rs @@ -501,8 +501,11 @@ mod trigger { pub use self::model::*; use super::*; + // type alias required by `Filter` macro + type TriggerMetadataChanged = MetadataChanged; + data_event! { - #[has_origin(origin = Trigger)] + #[has_origin(origin = Trigger)] pub enum TriggerEvent { Created(TriggerId), Deleted(TriggerId), @@ -510,6 +513,10 @@ mod trigger { Extended(TriggerNumberOfExecutionsChanged), #[has_origin(number_of_executions_changed => &number_of_executions_changed.trigger_id)] Shortened(TriggerNumberOfExecutionsChanged), + #[has_origin(metadata_changed => &metadata_changed.target_id)] + MetadataInserted(TriggerMetadataChanged), + #[has_origin(metadata_changed => &metadata_changed.target_id)] + MetadataRemoved(TriggerMetadataChanged), } } diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index 5be4f6bc159..7c61e86d266 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -238,6 +238,7 @@ pub mod parameter { pub const WSV_ASSET_DEFINITION_METADATA_LIMITS: &str = "WSVAssetDefinitionMetadataLimits"; pub const WSV_ACCOUNT_METADATA_LIMITS: &str = "WSVAccountMetadataLimits"; pub const WSV_DOMAIN_METADATA_LIMITS: &str = "WSVDomainMetadataLimits"; + pub const WSV_TRIGGER_METADATA_LIMITS: &str = "WSVTriggerMetadataLimits"; pub const WSV_IDENT_LENGTH_LIMITS: &str = "WSVIdentLengthLimits"; pub const WASM_FUEL_LIMIT: &str = "WASMFuelLimit"; pub const WASM_MAX_MEMORY: &str = "WASMMaxMemory"; diff --git a/data_model/src/smart_contract.rs b/data_model/src/smart_contract.rs index f159fa69531..0bbb807b0d5 100644 --- a/data_model/src/smart_contract.rs +++ b/data_model/src/smart_contract.rs @@ -17,6 +17,8 @@ pub mod payloads { /// Payload for trigger entrypoint #[derive(Debug, Clone, Encode, Decode)] pub struct Trigger { + /// Id of this trigger + pub id: TriggerId, /// Trigger owner who registered the trigger pub owner: AccountId, /// Event which triggered the execution diff --git a/data_model/src/visit.rs b/data_model/src/visit.rs index 62a33f328c8..e8a48e1da0b 100644 --- a/data_model/src/visit.rs +++ b/data_model/src/visit.rs @@ -147,12 +147,14 @@ pub trait Visit: ExpressionEvaluator { visit_set_account_key_value(SetKeyValue), visit_set_asset_definition_key_value(SetKeyValue), visit_set_asset_key_value(SetKeyValue), + visit_set_trigger_key_value(SetKeyValue>), // Visit RemoveKeyValueExpr visit_remove_domain_key_value(RemoveKeyValue), visit_remove_account_key_value(RemoveKeyValue), visit_remove_asset_definition_key_value(RemoveKeyValue), visit_remove_asset_key_value(RemoveKeyValue), + visit_remove_trigger_key_value(RemoveKeyValue>), // Visit GrantExpr visit_grant_account_permission(Grant), @@ -575,6 +577,14 @@ pub fn visit_set_key_value( value, }, ), + IdBox::TriggerId(object_id) => visitor.visit_set_trigger_key_value( + authority, + SetKeyValue { + object_id, + key, + value, + }, + ), _ => visitor.visit_unsupported(authority, isi), } } @@ -599,6 +609,9 @@ pub fn visit_remove_key_value( IdBox::DomainId(object_id) => { visitor.visit_remove_domain_key_value(authority, RemoveKeyValue { object_id, key }); } + IdBox::TriggerId(object_id) => { + visitor.visit_remove_trigger_key_value(authority, RemoveKeyValue { object_id, key }); + } _ => visitor.visit_unsupported(authority, isi), } } @@ -710,6 +723,8 @@ leaf_visitors! { visit_transfer_asset(Transfer), visit_set_asset_key_value(SetKeyValue), visit_remove_asset_key_value(RemoveKeyValue), + visit_set_trigger_key_value(SetKeyValue>), + visit_remove_trigger_key_value(RemoveKeyValue>), visit_register_asset_definition(Register), visit_unregister_asset_definition(Unregister), visit_transfer_asset_definition(Transfer), diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index 17d8eb33db2..9c66e941058 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -2833,6 +2833,22 @@ } ] }, + "MetadataChanged": { + "Struct": [ + { + "name": "target_id", + "type": "TriggerId" + }, + { + "name": "key", + "type": "Name" + }, + { + "name": "value", + "type": "Value" + } + ] + }, "MetadataError": { "Enum": [ { @@ -4418,6 +4434,16 @@ "tag": "Shortened", "discriminant": 3, "type": "TriggerNumberOfExecutionsChanged" + }, + { + "tag": "MetadataInserted", + "discriminant": 4, + "type": "MetadataChanged" + }, + { + "tag": "MetadataRemoved", + "discriminant": 5, + "type": "MetadataChanged" } ] }, @@ -4438,6 +4464,14 @@ { "tag": "ByShortened", "discriminant": 3 + }, + { + "tag": "ByMetadataInserted", + "discriminant": 4 + }, + { + "tag": "ByMetadataRemoved", + "discriminant": 5 } ] }, diff --git a/schema/gen/src/lib.rs b/schema/gen/src/lib.rs index ade793e5963..7ef9e9ec6ff 100644 --- a/schema/gen/src/lib.rs +++ b/schema/gen/src/lib.rs @@ -246,6 +246,7 @@ types!( MetadataChanged, MetadataChanged, MetadataChanged, + MetadataChanged, MetadataLimits, MintExpr, Mintable, diff --git a/smart_contract/executor/derive/src/default.rs b/smart_contract/executor/derive/src/default.rs index 2e458f86992..1b0262efa3b 100644 --- a/smart_contract/executor/derive/src/default.rs +++ b/smart_contract/executor/derive/src/default.rs @@ -138,6 +138,8 @@ pub fn impl_derive_visit(emitter: &mut Emitter, input: &syn2::DeriveInput) -> To "fn visit_transfer_asset(operation: Transfer)", "fn visit_set_asset_key_value(operation: SetKeyValue)", "fn visit_remove_asset_key_value(operation: RemoveKeyValue)", + "fn visit_set_trigger_key_value(operation: SetKeyValue>)", + "fn visit_remove_trigger_key_value(operation: RemoveKeyValue>)", "fn visit_unregister_asset_definition(operation: Unregister)", "fn visit_transfer_asset_definition(operation: Transfer)", "fn visit_set_asset_definition_key_value(operation: SetKeyValue)", diff --git a/smart_contract/executor/src/default.rs b/smart_contract/executor/src/default.rs index 603db7b670c..f0f1df28a2a 100644 --- a/smart_contract/executor/src/default.rs +++ b/smart_contract/executor/src/default.rs @@ -32,7 +32,7 @@ pub use role::{ }; pub use trigger::{ visit_burn_trigger_repetitions, visit_execute_trigger, visit_mint_trigger_repetitions, - visit_unregister_trigger, + visit_remove_trigger_key_value, visit_set_trigger_key_value, visit_unregister_trigger, }; use crate::{permission, permission::Token as _, prelude::*}; @@ -312,9 +312,7 @@ pub mod peer { pub mod domain { use iroha_smart_contract::data_model::{domain::DomainId, permission::PermissionToken}; - use permission::{ - accounts_permission_tokens, domain::is_domain_owner, - }; + use permission::{accounts_permission_tokens, domain::is_domain_owner}; use tokens::AnyPermissionToken; use super::*; @@ -1448,6 +1446,60 @@ pub mod trigger { deny!(executor, "Can't execute trigger owned by another account"); } + pub fn visit_set_trigger_key_value( + executor: &mut V, + authority: &AccountId, + isi: SetKeyValue>, + ) { + let trigger_id = isi.object_id; + + if is_genesis(executor) { + pass!(executor); + } + match is_trigger_owner(&trigger_id, authority) { + Err(err) => deny!(executor, err), + Ok(true) => pass!(executor), + Ok(false) => {} + } + let can_set_key_value_in_user_trigger_token = + tokens::trigger::CanSetKeyValueInTrigger { trigger_id }; + if can_set_key_value_in_user_trigger_token.is_owned_by(authority) { + pass!(executor); + } + + deny!( + executor, + "Can't set value to the metadata of another trigger" + ); + } + + pub fn visit_remove_trigger_key_value( + executor: &mut V, + authority: &AccountId, + isi: RemoveKeyValue>, + ) { + let trigger_id = isi.object_id; + + if is_genesis(executor) { + pass!(executor); + } + match is_trigger_owner(&trigger_id, authority) { + Err(err) => deny!(executor, err), + Ok(true) => pass!(executor), + Ok(false) => {} + } + let can_remove_key_value_in_trigger_token = + tokens::trigger::CanRemoveKeyValueInTrigger { trigger_id }; + if can_remove_key_value_in_trigger_token.is_owned_by(authority) { + pass!(executor); + } + + deny!( + executor, + "Can't remove value from the metadata of another trigger" + ); + } + fn is_token_trigger_associated(permission: &PermissionToken, trigger_id: &TriggerId) -> bool { let Ok(permission) = AnyPermissionToken::try_from(permission.clone()) else { return false; diff --git a/smart_contract/executor/src/default/tokens.rs b/smart_contract/executor/src/default/tokens.rs index a3a9645b204..bc486ada1ef 100644 --- a/smart_contract/executor/src/default/tokens.rs +++ b/smart_contract/executor/src/default/tokens.rs @@ -473,11 +473,29 @@ pub mod trigger { } } + token! { + #[derive(ValidateGrantRevoke)] + #[validate(permission::trigger::Owner)] + pub struct CanSetKeyValueInTrigger { + pub trigger_id: TriggerId, + } + } + + token! { + #[derive(ValidateGrantRevoke)] + #[validate(permission::trigger::Owner)] + pub struct CanRemoveKeyValueInTrigger { + pub trigger_id: TriggerId, + } + } + impl_froms!( CanExecuteUserTrigger, CanUnregisterUserTrigger, CanMintUserTrigger, CanBurnUserTrigger, + CanSetKeyValueInTrigger, + CanRemoveKeyValueInTrigger, ); } diff --git a/smart_contract/trigger/derive/src/entrypoint.rs b/smart_contract/trigger/derive/src/entrypoint.rs index 71387f19be9..6f5c07b1f4a 100644 --- a/smart_contract/trigger/derive/src/entrypoint.rs +++ b/smart_contract/trigger/derive/src/entrypoint.rs @@ -40,7 +40,7 @@ pub fn impl_entrypoint(_attr: TokenStream, item: TokenStream) -> TokenStream { #[doc(hidden)] unsafe extern "C" fn #main_fn_name() { let payload = ::iroha_trigger::get_trigger_payload(); - #fn_name(payload.owner, payload.event) + #fn_name(payload.id, payload.owner, payload.event) } // NOTE: Host objects are always passed by value to wasm diff --git a/tools/kagami/src/genesis.rs b/tools/kagami/src/genesis.rs index 41ff4c71237..ecefad84fe2 100644 --- a/tools/kagami/src/genesis.rs +++ b/tools/kagami/src/genesis.rs @@ -186,6 +186,7 @@ pub fn generate_default(executor: ExecutorMode) -> color_eyre::Result