diff --git a/Cargo.lock b/Cargo.lock index e40527e952e..0284f24a6be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1605,6 +1605,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "executor_custom_data_model" +version = "2.0.0-pre-rc.21" +dependencies = [ + "iroha_data_model", + "iroha_schema", + "serde", + "serde_json", +] + [[package]] name = "expect-test" version = "1.5.0" @@ -2785,6 +2795,7 @@ dependencies = [ "derive_more", "displaydoc 0.2.4 (git+https://github.com/akonradi-signal/displaydoc.git?branch=anonymous-const)", "error-stack", + "executor_custom_data_model", "eyre", "futures-util", "hex", diff --git a/client/Cargo.toml b/client/Cargo.toml index 1243081020f..42c820a2a0d 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -90,6 +90,7 @@ irohad = { workspace = true } iroha_wasm_builder = { workspace = true } iroha_genesis = { workspace = true } test_network = { workspace = true } +executor_custom_data_model = { path = "tests/integration/smartcontracts/executor_custom_data_model" } tokio = { workspace = true, features = ["rt-multi-thread"] } criterion = { workspace = true, features = ["html_reports"] } diff --git a/client/tests/integration/smartcontracts/Cargo.toml b/client/tests/integration/smartcontracts/Cargo.toml index a03698db1b2..5004748a0c0 100644 --- a/client/tests/integration/smartcontracts/Cargo.toml +++ b/client/tests/integration/smartcontracts/Cargo.toml @@ -15,6 +15,9 @@ members = [ "executor_with_custom_permission", "executor_remove_permission", "executor_with_migration_fail", + "executor_custom_instructions_simple", + "executor_custom_instructions_complex", + "executor_custom_data_model", "query_assets_and_save_cursor", "smart_contract_can_filter_queries", ] @@ -32,8 +35,10 @@ codegen-units = 1 # Further reduces binary size but increases compilation time [workspace.dependencies] iroha_smart_contract = { version = "=2.0.0-pre-rc.21", path = "../../../../smart_contract", features = ["debug"] } iroha_trigger = { version = "=2.0.0-pre-rc.21", path = "../../../../smart_contract/trigger", features = ["debug"] } -iroha_executor = { version = "=2.0.0-pre-rc.21", path = "../../../../smart_contract/executor" } +iroha_executor = { version = "=2.0.0-pre-rc.21", path = "../../../../smart_contract/executor", features = ["debug"] } iroha_schema = { version = "=2.0.0-pre-rc.21", path = "../../../../schema" } +iroha_data_model = { version = "=2.0.0-pre-rc.21", path = "../../../../data_model", default-features = false } +executor_custom_data_model = { version = "=2.0.0-pre-rc.21", path = "executor_custom_data_model" } parity-scale-codec = { version = "3.2.1", default-features = false } anyhow = { version = "1.0.71", default-features = false } diff --git a/client/tests/integration/smartcontracts/executor_custom_data_model/Cargo.toml b/client/tests/integration/smartcontracts/executor_custom_data_model/Cargo.toml new file mode 100644 index 00000000000..6ce6deb6833 --- /dev/null +++ b/client/tests/integration/smartcontracts/executor_custom_data_model/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "executor_custom_data_model" + +edition.workspace = true +version.workspace = true +authors.workspace = true + +license.workspace = true + +[dependencies] +iroha_data_model.workspace = true +iroha_schema.workspace = true + +serde_json.workspace = true +serde.workspace = true diff --git a/client/tests/integration/smartcontracts/executor_custom_data_model/src/complex.rs b/client/tests/integration/smartcontracts/executor_custom_data_model/src/complex.rs new file mode 100644 index 00000000000..b86b689f4f0 --- /dev/null +++ b/client/tests/integration/smartcontracts/executor_custom_data_model/src/complex.rs @@ -0,0 +1,310 @@ +//! Example of custom expression system. +//! Only few expressions are implemented to show proof-of-concept. +//! See `smartcontracts/executor_custom_instructions_complex`. +//! This is simplified version of expression system from Iroha v2.0.0-pre-rc.20 + +pub use evaluate::*; +pub use expression::*; +pub use isi::*; + +mod isi { + use alloc::{boxed::Box, format, string::String, vec::Vec}; + + use iroha_data_model::{ + isi::{Custom, InstructionBox}, + JsonString, + }; + use iroha_schema::IntoSchema; + use serde::{Deserialize, Serialize}; + + use crate::complex::expression::EvaluatesTo; + + #[derive(Debug, Deserialize, Serialize, IntoSchema)] + pub enum CustomInstructionExpr { + Core(CoreExpr), + If(Box), + // Other custom instructions + } + + impl From for Custom { + fn from(isi: CustomInstructionExpr) -> Self { + let payload = + JsonString::serialize(&isi).expect("Couldn't serialize custom instruction"); + Self::new(payload) + } + } + + impl CustomInstructionExpr { + pub fn into_instruction(self) -> InstructionBox { + InstructionBox::Custom(self.into()) + } + } + + impl TryFrom<&JsonString> for CustomInstructionExpr { + type Error = serde_json::Error; + + fn try_from(payload: &JsonString) -> serde_json::Result { + payload.deserialize() + } + } + + // Built-in instruction (can be evaluated based on query values, etc) + #[derive(Debug, Deserialize, Serialize, IntoSchema)] + pub struct CoreExpr { + pub object: EvaluatesTo, + } + + impl CoreExpr { + pub fn new(object: impl Into>) -> Self { + Self { + object: object.into(), + } + } + } + + /// Composite instruction for a conditional execution of other instructions. + #[derive(Debug, Deserialize, Serialize, IntoSchema)] + pub struct ConditionalExpr { + /// Condition to be checked. + pub condition: EvaluatesTo, + /// Instruction to be executed if condition pass. + pub then: CustomInstructionExpr, + } + + impl ConditionalExpr { + pub fn new( + condition: impl Into>, + then: impl Into, + ) -> Self { + Self { + condition: condition.into(), + then: then.into(), + } + } + } +} + +mod expression { + use alloc::{ + boxed::Box, + format, + string::{String, ToString}, + vec, + vec::Vec, + }; + use core::marker::PhantomData; + + use iroha_data_model::{ + isi::InstructionBox, + prelude::{Numeric, QueryBox}, + }; + use iroha_schema::{IntoSchema, TypeId}; + use serde::{Deserialize, Serialize}; + + /// Struct for type checking and converting expression results. + #[derive(Debug, Deserialize, Serialize, TypeId)] + pub struct EvaluatesTo { + #[serde(flatten)] + pub(crate) expression: Box, + _value_type: PhantomData, + } + + impl EvaluatesTo { + pub fn new_unchecked(expression: impl Into) -> Self { + Self { + expression: Box::new(expression.into()), + _value_type: PhantomData, + } + } + } + + /// Represents all possible expressions. + #[derive(Debug, Deserialize, Serialize, IntoSchema)] + pub enum Expression { + /// Raw value. + Raw(Value), + /// Greater expression. + Greater(Greater), + /// Query to Iroha state. + Query(QueryBox), + } + + /// Returns whether the `left` expression is greater than the `right`. + #[derive(Debug, Deserialize, Serialize, IntoSchema)] + pub struct Greater { + pub left: EvaluatesTo, + pub right: EvaluatesTo, + } + + impl Greater { + /// Construct new [`Greater`] expression + pub fn new( + left: impl Into>, + right: impl Into>, + ) -> Self { + Self { + left: left.into(), + right: right.into(), + } + } + } + + impl From for EvaluatesTo { + fn from(expression: Greater) -> Self { + let expression = Expression::Greater(expression); + EvaluatesTo::new_unchecked(expression) + } + } + + /// Sized container for all possible values. + #[derive(Debug, Clone, Deserialize, Serialize, IntoSchema)] + pub enum Value { + Bool(bool), + Numeric(Numeric), + InstructionBox(InstructionBox), + } + + impl From for Value { + fn from(value: bool) -> Self { + Value::Bool(value) + } + } + + impl From for EvaluatesTo { + fn from(value: Numeric) -> Self { + let value = Value::Numeric(value); + let expression = Expression::Raw(value); + EvaluatesTo::new_unchecked(expression) + } + } + + impl From for EvaluatesTo { + fn from(value: InstructionBox) -> Self { + let value = Value::InstructionBox(value); + let expression = Expression::Raw(value); + EvaluatesTo::new_unchecked(expression) + } + } + + impl TryFrom for bool { + type Error = String; + + fn try_from(value: Value) -> Result { + match value { + Value::Bool(value) => Ok(value), + _ => Err("Expected bool".to_string()), + } + } + } + + impl TryFrom for Numeric { + type Error = String; + + fn try_from(value: Value) -> Result { + match value { + Value::Numeric(value) => Ok(value), + _ => Err("Expected Numeric".to_string()), + } + } + } + + impl TryFrom for InstructionBox { + type Error = String; + + fn try_from(value: Value) -> Result { + match value { + Value::InstructionBox(value) => Ok(value), + _ => Err("Expected InstructionBox".to_string()), + } + } + } + + impl + IntoSchema> IntoSchema for EvaluatesTo { + fn type_name() -> String { + format!("EvaluatesTo<{}>", V::type_name()) + } + fn update_schema_map(map: &mut iroha_schema::MetaMap) { + const EXPRESSION: &str = "expression"; + + if !map.contains_key::() { + map.insert::(iroha_schema::Metadata::Struct( + iroha_schema::NamedFieldsMeta { + declarations: vec![iroha_schema::Declaration { + name: String::from(EXPRESSION), + ty: core::any::TypeId::of::(), + }], + }, + )); + + Expression::update_schema_map(map); + } + } + } +} + +mod evaluate { + use alloc::string::ToString; + + use iroha_data_model::{ + isi::error::InstructionExecutionError, query::QueryBox, ValidationFail, + }; + + use crate::complex::expression::{EvaluatesTo, Expression, Greater, Value}; + + pub trait Evaluate { + /// The resulting type of the expression. + type Value; + + /// Calculate result. + fn evaluate(&self, context: &impl Context) -> Result; + } + + pub trait Context { + /// Execute query against the current state of `Iroha` + fn query(&self, query: &QueryBox) -> Result; + } + + impl> Evaluate for EvaluatesTo + where + V::Error: ToString, + { + type Value = V; + + fn evaluate(&self, context: &impl Context) -> Result { + let expr = self.expression.evaluate(context)?; + V::try_from(expr) + .map_err(|e| InstructionExecutionError::Conversion(e.to_string())) + .map_err(ValidationFail::InstructionFailed) + } + } + + impl Evaluate for Expression { + type Value = Value; + + fn evaluate(&self, context: &impl Context) -> Result { + match self { + Expression::Raw(value) => Ok(value.clone()), + Expression::Greater(expr) => expr.evaluate(context).map(Into::into), + Expression::Query(expr) => expr.evaluate(context), + } + } + } + + impl Evaluate for Greater { + type Value = bool; + + fn evaluate(&self, context: &impl Context) -> Result { + let left = self.left.evaluate(context)?; + let right = self.right.evaluate(context)?; + Ok(left > right) + } + } + + impl Evaluate for QueryBox { + type Value = Value; + + fn evaluate(&self, context: &impl Context) -> Result { + context.query(self) + } + } +} diff --git a/client/tests/integration/smartcontracts/executor_custom_data_model/src/lib.rs b/client/tests/integration/smartcontracts/executor_custom_data_model/src/lib.rs new file mode 100644 index 00000000000..62405125d13 --- /dev/null +++ b/client/tests/integration/smartcontracts/executor_custom_data_model/src/lib.rs @@ -0,0 +1,8 @@ +//! Example of custom instructions which can be used in custom executor + +#![no_std] + +extern crate alloc; + +pub mod complex; +pub mod simple; diff --git a/client/tests/integration/smartcontracts/executor_custom_data_model/src/simple.rs b/client/tests/integration/smartcontracts/executor_custom_data_model/src/simple.rs new file mode 100644 index 00000000000..21b63095307 --- /dev/null +++ b/client/tests/integration/smartcontracts/executor_custom_data_model/src/simple.rs @@ -0,0 +1,46 @@ +//! Example of one custom instruction. +//! See `smartcontracts/executor_custom_instructions_simple`. + +use alloc::{format, string::String, vec::Vec}; + +use iroha_data_model::{ + asset::AssetDefinitionId, + isi::{Custom, InstructionBox}, + prelude::Numeric, + JsonString, +}; +use iroha_schema::IntoSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, IntoSchema)] +pub enum CustomInstructionBox { + MintAssetForAllAccounts(MintAssetForAllAccounts), + // Other custom instructions +} + +#[derive(Debug, Deserialize, Serialize, IntoSchema)] +pub struct MintAssetForAllAccounts { + pub asset_definition_id: AssetDefinitionId, + pub quantity: Numeric, +} + +impl From for Custom { + fn from(isi: CustomInstructionBox) -> Self { + let payload = JsonString::serialize(&isi).expect("Couldn't serialize custom instruction"); + Self::new(payload) + } +} + +impl CustomInstructionBox { + pub fn into_instruction(self) -> InstructionBox { + InstructionBox::Custom(self.into()) + } +} + +impl TryFrom<&JsonString> for CustomInstructionBox { + type Error = serde_json::Error; + + fn try_from(payload: &JsonString) -> serde_json::Result { + payload.deserialize() + } +} diff --git a/client/tests/integration/smartcontracts/executor_custom_instructions_complex/Cargo.toml b/client/tests/integration/smartcontracts/executor_custom_instructions_complex/Cargo.toml new file mode 100644 index 00000000000..08d487860ee --- /dev/null +++ b/client/tests/integration/smartcontracts/executor_custom_instructions_complex/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "executor_custom_instructions_complex" + +edition.workspace = true +version.workspace = true +authors.workspace = true + +license.workspace = true + +[lib] +crate-type = ['cdylib'] + +[dependencies] +iroha_executor.workspace = true +iroha_schema.workspace = true +executor_custom_data_model.workspace = true + +serde_json.workspace = true +serde.workspace = true + +panic-halt.workspace = true +lol_alloc.workspace = true +getrandom.workspace = true diff --git a/client/tests/integration/smartcontracts/executor_custom_instructions_complex/src/lib.rs b/client/tests/integration/smartcontracts/executor_custom_instructions_complex/src/lib.rs new file mode 100644 index 00000000000..13cf48b8fab --- /dev/null +++ b/client/tests/integration/smartcontracts/executor_custom_instructions_complex/src/lib.rs @@ -0,0 +1,86 @@ +//! Runtime Executor which extends instruction set with simple expression system. +//! Example of custom expression: +//! "If specific user has more then X amount of specific asset, burn Y amount of that asset" +//! This is expressed as [ConditionalExpr] with [Expression::Greater] and [Expression::Query] as condition. +//! Note that only few expressions are implemented to demonstrate proof-of-concept. + +#![no_std] + +extern crate alloc; +#[cfg(not(test))] +extern crate panic_halt; + +use executor_custom_data_model::complex::{ + ConditionalExpr, CoreExpr, CustomInstructionExpr, Evaluate, Value, +}; +use iroha_executor::{ + data_model::{isi::Custom, query::QueryOutputBox}, + prelude::*, +}; +use lol_alloc::{FreeListAllocator, LockedAllocator}; + +#[global_allocator] +static ALLOC: LockedAllocator = LockedAllocator::new(FreeListAllocator::new()); + +getrandom::register_custom_getrandom!(iroha_executor::stub_getrandom); + +#[derive(Constructor, ValidateEntrypoints, Validate, Visit)] +#[visit(custom(visit_custom))] +struct Executor { + verdict: Result, + block_height: u64, +} + +fn visit_custom(executor: &mut Executor, _authority: &AccountId, isi: &Custom) { + let Ok(isi) = CustomInstructionExpr::try_from(isi.payload()) else { + deny!(executor, "Failed to parse custom instruction"); + }; + match execute_custom_instruction(isi) { + Ok(()) => return, + Err(err) => { + deny!(executor, err); + } + } +} + +fn execute_custom_instruction(isi: CustomInstructionExpr) -> Result<(), ValidationFail> { + match isi { + CustomInstructionExpr::Core(isi) => execute_core(isi), + CustomInstructionExpr::If(isi) => execute_if(*isi), + } +} + +fn execute_core(isi: CoreExpr) -> Result<(), ValidationFail> { + let isi = isi.object.evaluate(&Context)?; + isi.execute() +} + +fn execute_if(isi: ConditionalExpr) -> Result<(), ValidationFail> { + let condition = isi.condition.evaluate(&Context)?; + if condition { + execute_custom_instruction(isi.then) + } else { + Ok(()) + } +} + +struct Context; + +impl executor_custom_data_model::complex::Context for Context { + fn query(&self, query: &QueryBox) -> Result { + // Note: supported only queries which return numeric result + match query.execute()?.into_inner() { + QueryOutputBox::Numeric(value) => Ok(Value::Numeric(value)), + _ => unimplemented!(), + } + } +} + +#[entrypoint] +pub fn migrate(_block_height: u64) -> MigrationResult { + DataModelBuilder::with_default_permissions() + .with_custom_instruction::() + .build_and_set(); + + Ok(()) +} diff --git a/client/tests/integration/smartcontracts/executor_custom_instructions_simple/Cargo.toml b/client/tests/integration/smartcontracts/executor_custom_instructions_simple/Cargo.toml new file mode 100644 index 00000000000..acae74a475c --- /dev/null +++ b/client/tests/integration/smartcontracts/executor_custom_instructions_simple/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "executor_custom_instructions_simple" + +edition.workspace = true +version.workspace = true +authors.workspace = true + +license.workspace = true + +[lib] +crate-type = ['cdylib'] + +[dependencies] +iroha_executor.workspace = true +iroha_schema.workspace = true +executor_custom_data_model.workspace = true + +serde_json.workspace = true +serde.workspace = true + +panic-halt.workspace = true +lol_alloc.workspace = true +getrandom.workspace = true diff --git a/client/tests/integration/smartcontracts/executor_custom_instructions_simple/src/lib.rs b/client/tests/integration/smartcontracts/executor_custom_instructions_simple/src/lib.rs new file mode 100644 index 00000000000..37c7c1bdfc0 --- /dev/null +++ b/client/tests/integration/smartcontracts/executor_custom_instructions_simple/src/lib.rs @@ -0,0 +1,64 @@ +//! Runtime Executor which extends instruction set with one custom instruction - [MintAssetForAllAccounts]. +//! This instruction is handled in executor, and translates to multiple usual ISIs. +//! It is possible to use queries during execution. + +#![no_std] + +extern crate alloc; +#[cfg(not(test))] +extern crate panic_halt; + +use executor_custom_data_model::simple::{CustomInstructionBox, MintAssetForAllAccounts}; +use iroha_executor::{data_model::isi::Custom, debug::DebugExpectExt, prelude::*}; +use lol_alloc::{FreeListAllocator, LockedAllocator}; + +#[global_allocator] +static ALLOC: LockedAllocator = LockedAllocator::new(FreeListAllocator::new()); + +getrandom::register_custom_getrandom!(iroha_executor::stub_getrandom); + +#[derive(Constructor, ValidateEntrypoints, Validate, Visit)] +#[visit(custom(visit_custom))] +struct Executor { + verdict: Result, + block_height: u64, +} + +fn visit_custom(executor: &mut Executor, _authority: &AccountId, isi: &Custom) { + let Ok(isi) = CustomInstructionBox::try_from(isi.payload()) else { + deny!(executor, "Failed to parse custom instruction"); + }; + match execute_custom_instruction(isi) { + Ok(()) => return, + Err(err) => { + deny!(executor, err); + } + } +} + +fn execute_custom_instruction(isi: CustomInstructionBox) -> Result<(), ValidationFail> { + match isi { + CustomInstructionBox::MintAssetForAllAccounts(isi) => { + execute_mint_asset_for_all_accounts(isi) + } + } +} + +fn execute_mint_asset_for_all_accounts(isi: MintAssetForAllAccounts) -> Result<(), ValidationFail> { + let accounts = FindAccountsWithAsset::new(isi.asset_definition_id.clone()).execute()?; + for account in accounts { + let account = account.dbg_expect("Failed to get accounts with asset"); + let asset_id = AssetId::new(isi.asset_definition_id.clone(), account.id().clone()); + Mint::asset_numeric(isi.quantity, asset_id).execute()?; + } + Ok(()) +} + +#[entrypoint] +pub fn migrate(_block_height: u64) -> MigrationResult { + DataModelBuilder::with_default_permissions() + .with_custom_instruction::() + .build_and_set(); + + Ok(()) +} diff --git a/client/tests/integration/upgrade.rs b/client/tests/integration/upgrade.rs index 1a4f998d87f..23954f4ac08 100644 --- a/client/tests/integration/upgrade.rs +++ b/client/tests/integration/upgrade.rs @@ -7,10 +7,11 @@ use iroha::{ crypto::KeyPair, data_model::prelude::*, }; +use iroha_data_model::parameter::{default::EXECUTOR_FUEL_LIMIT, ParametersBuilder}; use iroha_logger::info; use serde_json::json; use test_network::*; -use test_samples::ALICE_ID; +use test_samples::{ALICE_ID, BOB_ID}; use tokio::sync::mpsc; const ADMIN_PUBLIC_KEY_MULTIHASH: &str = @@ -197,6 +198,121 @@ fn executor_upgrade_should_revoke_removed_permissions() -> Result<()> { Ok(()) } +#[test] +fn executor_custom_instructions_simple() -> Result<()> { + use executor_custom_data_model::simple::{CustomInstructionBox, MintAssetForAllAccounts}; + + let (_rt, _peer, client) = ::new().with_port(11_270).start_with_runtime(); + wait_for_genesis_committed(&vec![client.clone()], 0); + + upgrade_executor( + &client, + "tests/integration/smartcontracts/executor_custom_instructions_simple", + )?; + + let asset_definition_id: AssetDefinitionId = "rose#wonderland".parse().unwrap(); + + // Give 1 rose to bob + let bob_rose = AssetId::new(asset_definition_id.clone(), BOB_ID.clone()); + client.submit_blocking(Mint::asset_numeric(Numeric::from(1u32), bob_rose.clone()))?; + + // Check that bob has 1 rose + assert_eq!( + client.request(FindAssetQuantityById::new(bob_rose.clone()))?, + Numeric::from(1u32) + ); + + // Give 1 rose to all + let isi = MintAssetForAllAccounts { + asset_definition_id, + quantity: Numeric::from(1u32), + }; + let isi = CustomInstructionBox::MintAssetForAllAccounts(isi); + client.submit_blocking(isi.into_instruction())?; + + // Check that bob has 2 roses + assert_eq!( + client.request(FindAssetQuantityById::new(bob_rose.clone()))?, + Numeric::from(2u32) + ); + + Ok(()) +} + +#[test] +fn executor_custom_instructions_complex() -> Result<()> { + use executor_custom_data_model::complex::{ + ConditionalExpr, CoreExpr, CustomInstructionExpr, EvaluatesTo, Expression, Greater, + }; + use iroha_config::parameters::actual::Root as Config; + + let mut config = Config::test(); + // Note that this value will be overwritten by genesis block with NewParameter ISI + // But it will be needed after NewParameter removal in #4597 + config.chain_wide.executor_runtime.fuel_limit = 1_000_000_000; + + let (_rt, _peer, client) = PeerBuilder::new() + .with_port(11_275) + .with_config(config) + .start_with_runtime(); + wait_for_genesis_committed(&vec![client.clone()], 0); + + // Remove this after #4597 - config value will be used (see above) + let parameters = ParametersBuilder::new() + .add_parameter(EXECUTOR_FUEL_LIMIT, Numeric::from(1_000_000_000_u32))? + .into_set_parameters(); + client.submit_all_blocking(parameters)?; + + upgrade_executor( + &client, + "tests/integration/smartcontracts/executor_custom_instructions_complex", + )?; + + // Give 6 roses to bob + let asset_definition_id: AssetDefinitionId = "rose#wonderland".parse().unwrap(); + let bob_rose = AssetId::new(asset_definition_id.clone(), BOB_ID.clone()); + client.submit_blocking(Mint::asset_numeric(Numeric::from(6u32), bob_rose.clone()))?; + + // Check that bob has 6 roses + assert_eq!( + client.request(FindAssetQuantityById::new(bob_rose.clone()))?, + Numeric::from(6u32) + ); + + // If bob has more then 5 roses, then burn 1 rose + let burn_bob_rose_if_more_then_5 = || -> Result<()> { + let condition = Greater::new( + EvaluatesTo::new_unchecked(Expression::Query( + FindAssetQuantityById::new(bob_rose.clone()).into(), + )), + Numeric::from(5u32), + ); + let then = Burn::asset_numeric(Numeric::from(1u32), bob_rose.clone()); + let then: InstructionBox = then.into(); + let then = CustomInstructionExpr::Core(CoreExpr::new(then)); + let isi = CustomInstructionExpr::If(Box::new(ConditionalExpr::new(condition, then))); + client.submit_blocking(isi.into_instruction())?; + Ok(()) + }; + burn_bob_rose_if_more_then_5()?; + + // Check that bob has 5 roses + assert_eq!( + client.request(FindAssetQuantityById::new(bob_rose.clone()))?, + Numeric::from(5u32) + ); + + burn_bob_rose_if_more_then_5()?; + + // Check that bob has 5 roses + assert_eq!( + client.request(FindAssetQuantityById::new(bob_rose.clone()))?, + Numeric::from(5u32) + ); + + Ok(()) +} + #[test] fn migration_fail_should_not_cause_any_effects() { let (_rt, _peer, client) = ::new().with_port(10_998).start_with_runtime(); diff --git a/configs/swarm/executor.wasm b/configs/swarm/executor.wasm index 4c4c8b26ecf..eb20b37f59d 100644 Binary files a/configs/swarm/executor.wasm and b/configs/swarm/executor.wasm differ diff --git a/core/src/smartcontracts/isi/mod.rs b/core/src/smartcontracts/isi/mod.rs index 659f778ecea..46acb4a1fc0 100644 --- a/core/src/smartcontracts/isi/mod.rs +++ b/core/src/smartcontracts/isi/mod.rs @@ -58,6 +58,9 @@ impl Execute for InstructionBox { Self::NewParameter(isi) => isi.execute(authority, state_transaction), Self::Upgrade(isi) => isi.execute(authority, state_transaction), Self::Log(isi) => isi.execute(authority, state_transaction), + Self::Custom(_) => { + panic!("Custom instructions should be handled in custom executor"); + } } } } diff --git a/core/test_network/src/lib.rs b/core/test_network/src/lib.rs index 911d10ee45e..5bec6392ab5 100644 --- a/core/test_network/src/lib.rs +++ b/core/test_network/src/lib.rs @@ -782,15 +782,20 @@ impl TestConfig for Config { } } +// Increased timeout to prevent flaky tests +const TRANSACTION_STATUS_TIMEOUT: Duration = Duration::from_secs(150); + impl TestClientConfig for ClientConfig { fn test(api_address: &SocketAddr) -> Self { - iroha::samples::get_client_config( + let mut config = iroha::samples::get_client_config( get_chain_id(), get_key_pair(Signatory::Alice), format!("http://{api_address}") .parse() .expect("should be valid url"), - ) + ); + config.transaction_status_timeout = TRANSACTION_STATUS_TIMEOUT; + config } } diff --git a/data_model/src/executor.rs b/data_model/src/executor.rs index d0c0b225efd..4e030b1c3b6 100644 --- a/data_model/src/executor.rs +++ b/data_model/src/executor.rs @@ -17,6 +17,8 @@ use crate::{permission::PermissionId, transaction::WasmSmartContract, JsonString #[model] mod model { + use iroha_schema::Ident; + use super::*; /// executor that checks if an operation satisfies some conditions. @@ -81,6 +83,14 @@ mod model { /// /// These IDs refer to the types in the schema. pub permissions: BTreeSet, + /// Type id in the schema. + /// Corresponds to payload of `InstructionBox::Custom`. + /// + /// Note that technically it is not needed + /// (custom instructions can be used without specifying it), + /// however it is recommended to set it, + /// so clients could retrieve it through Iroha API. + pub custom_instruction: Option, /// Data model JSON schema, typically produced by [`IntoSchema`]. pub schema: JsonString, } diff --git a/data_model/src/isi.rs b/data_model/src/isi.rs index 7ca016b30b2..07bb86c0385 100644 --- a/data_model/src/isi.rs +++ b/data_model/src/isi.rs @@ -116,6 +116,8 @@ mod model { Upgrade(Upgrade), #[debug(fmt = "{_0:?}")] Log(Log), + #[debug(fmt = "{_0:?}")] + Custom(Custom), #[debug(fmt = "{_0:?}")] Fail(Fail), @@ -176,6 +178,7 @@ impl_instruction! { Upgrade, ExecuteTrigger, Log, + Custom, Fail, } @@ -187,7 +190,7 @@ impl Instruction for InstructionBox { mod transparent { use super::*; - use crate::{account::NewAccount, domain::NewDomain, metadata::Metadata}; + use crate::{account::NewAccount, domain::NewDomain, metadata::Metadata, JsonString}; macro_rules! isi { ($($meta:meta)* $item:item) => { @@ -997,6 +1000,32 @@ mod transparent { pub msg: String, } } + + isi! { + /// Custom instruction with arbitrary payload. + /// Should be handled in custom executor, where it will be translated to usual ISIs. + /// Can be used to extend instruction set or add expression system. + /// See `executor_custom_instructions_simple` and `executor_custom_instructions_complex` + /// examples in `client/tests/integration/smartcontracts`. + /// + /// Note: If using custom instructions, it is recommended + /// to set `ExecutorDataModel::custom_instruction` in custom executor `migrate` entrypoint. + #[derive(Display)] + #[display(fmt = "CUSTOM({payload})")] + pub struct Custom { + /// Custom payload + pub payload: JsonString, + } + } + + impl Custom { + /// Constructor + pub fn new(payload: impl Into) -> Self { + Self { + payload: payload.into(), + } + } + } } macro_rules! isi_box { @@ -1518,9 +1547,9 @@ pub mod error { /// The prelude re-exports most commonly used traits, structs and macros from this crate. pub mod prelude { pub use super::{ - AssetTransferBox, Burn, BurnBox, ExecuteTrigger, Fail, Grant, GrantBox, InstructionBox, - Log, Mint, MintBox, NewParameter, Register, RegisterBox, RemoveKeyValue, RemoveKeyValueBox, - Revoke, RevokeBox, SetKeyValue, SetKeyValueBox, SetParameter, Transfer, TransferBox, - Unregister, UnregisterBox, Upgrade, + AssetTransferBox, Burn, BurnBox, Custom, ExecuteTrigger, Fail, Grant, GrantBox, + InstructionBox, Log, Mint, MintBox, NewParameter, Register, RegisterBox, RemoveKeyValue, + RemoveKeyValueBox, Revoke, RevokeBox, SetKeyValue, SetKeyValueBox, SetParameter, Transfer, + TransferBox, Unregister, UnregisterBox, Upgrade, }; } diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index c856e2f13f8..08973b8fee3 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -121,6 +121,7 @@ mod seal { Upgrade, ExecuteTrigger, Log, + Custom, Fail, // Boxed queries diff --git a/data_model/src/transaction.rs b/data_model/src/transaction.rs index 081e0a514f9..9b8bbc83c8c 100644 --- a/data_model/src/transaction.rs +++ b/data_model/src/transaction.rs @@ -583,6 +583,7 @@ pub mod error { NewParameter(_) => "new parameter", Upgrade(_) => "upgrade", Log(_) => "log", + Custom(_) => "custom", }; write!( f, diff --git a/data_model/src/visit.rs b/data_model/src/visit.rs index 7369ba377b5..bf0dcfacdf0 100644 --- a/data_model/src/visit.rs +++ b/data_model/src/visit.rs @@ -43,6 +43,7 @@ pub trait Visit { visit_new_parameter(&NewParameter), visit_set_parameter(&SetParameter), visit_log(&Log), + visit_custom(&Custom), // Visit QueryBox visit_find_account_by_id(&FindAccountById), @@ -259,6 +260,7 @@ pub fn visit_instruction( visitor.visit_unregister(authority, variant_value) } InstructionBox::Upgrade(variant_value) => visitor.visit_upgrade(authority, variant_value), + InstructionBox::Custom(custom) => visitor.visit_custom(authority, custom), } } @@ -431,6 +433,7 @@ leaf_visitors! { visit_execute_trigger(&ExecuteTrigger), visit_fail(&Fail), visit_log(&Log), + visit_custom(&Custom), // Query visitors visit_find_account_by_id(&FindAccountById), diff --git a/docs/source/references/schema.json b/docs/source/references/schema.json index 649fee7573d..cc30a9c8d14 100644 --- a/docs/source/references/schema.json +++ b/docs/source/references/schema.json @@ -833,6 +833,14 @@ } ] }, + "Custom": { + "Struct": [ + { + "name": "payload", + "type": "JsonString" + } + ] + }, "DataEvent": { "Enum": [ { @@ -1197,6 +1205,10 @@ "name": "permissions", "type": "SortedVec" }, + { + "name": "custom_instruction", + "type": "Option" + }, { "name": "schema", "type": "JsonString" @@ -1858,8 +1870,13 @@ "type": "Log" }, { - "tag": "Fail", + "tag": "Custom", "discriminant": 14, + "type": "Custom" + }, + { + "tag": "Fail", + "discriminant": 15, "type": "Fail" } ] @@ -2013,8 +2030,12 @@ "discriminant": 13 }, { - "tag": "Fail", + "tag": "Custom", "discriminant": 14 + }, + { + "tag": "Fail", + "discriminant": 15 } ] }, diff --git a/schema/gen/src/lib.rs b/schema/gen/src/lib.rs index c1735b9056e..df52d034650 100644 --- a/schema/gen/src/lib.rs +++ b/schema/gen/src/lib.rs @@ -117,6 +117,7 @@ types!( ConstVec, Container, ClientQueryPayload, + Custom, DataEvent, DataEventFilter, Domain, diff --git a/smart_contract/executor/derive/src/default.rs b/smart_contract/executor/derive/src/default.rs index 14b58d0bd73..6b436fc7019 100644 --- a/smart_contract/executor/derive/src/default.rs +++ b/smart_contract/executor/derive/src/default.rs @@ -159,6 +159,7 @@ pub fn impl_derive_visit(emitter: &mut Emitter, input: &syn::DeriveInput) -> Tok "fn visit_new_parameter(operation: &NewParameter)", "fn visit_upgrade(operation: &Upgrade)", "fn visit_log(operation: &Log)", + "fn visit_custom(operation: &Custom)", "fn visit_fail(operation: &Fail)", ] .into_iter() diff --git a/smart_contract/executor/src/default.rs b/smart_contract/executor/src/default.rs index 9ab7113e4c3..26c4a6707b3 100644 --- a/smart_contract/executor/src/default.rs +++ b/smart_contract/executor/src/default.rs @@ -19,6 +19,7 @@ pub use asset_definition::{ visit_set_asset_definition_key_value, visit_transfer_asset_definition, visit_unregister_asset_definition, }; +pub use custom::visit_custom; pub use domain::{ visit_register_domain, visit_remove_domain_key_value, visit_set_domain_key_value, visit_transfer_domain, visit_unregister_domain, @@ -130,6 +131,9 @@ pub fn visit_instruction( InstructionBox::Upgrade(isi) => { executor.visit_upgrade(authority, isi); } + InstructionBox::Custom(isi) => { + executor.visit_custom(authority, isi); + } } } @@ -1702,6 +1706,21 @@ pub mod log { } } +pub mod custom { + use super::*; + + pub fn visit_custom( + executor: &mut V, + _authority: &AccountId, + _isi: &Custom, + ) { + deny!( + executor, + "Custom instructions should be handled in custom executor" + ) + } +} + pub mod fail { use super::*; diff --git a/smart_contract/executor/src/lib.rs b/smart_contract/executor/src/lib.rs index 08281ae3b3d..6672ba232ad 100644 --- a/smart_contract/executor/src/lib.rs +++ b/smart_contract/executor/src/lib.rs @@ -10,6 +10,7 @@ use alloc::collections::BTreeSet; use data_model::{executor::Result, ValidationFail}; #[cfg(not(test))] use data_model::{prelude::*, smart_contract::payloads}; +use iroha_schema::Ident; pub use iroha_schema::MetaMap; pub use iroha_smart_contract as smart_contract; pub use iroha_smart_contract_utils::{debug, encode_with_length_prefix}; @@ -189,6 +190,7 @@ pub enum TryFromDataModelObjectError { pub struct DataModelBuilder { schema: MetaMap, permissions: BTreeSet, + custom_instruction: Option, } impl DataModelBuilder { @@ -199,6 +201,7 @@ impl DataModelBuilder { Self { schema: <_>::default(), permissions: <_>::default(), + custom_instruction: None, } } @@ -226,6 +229,15 @@ impl DataModelBuilder { self } + /// Define a type of custom instruction in the data model. + /// Corresponds to payload of `InstructionBox::Custom`. + #[must_use] + pub fn with_custom_instruction(mut self) -> Self { + T::update_schema_map(&mut self.schema); + self.custom_instruction = Some(T::type_name()); + self + } + /// Remove a permission from the data model #[must_use] pub fn remove_permission(mut self) -> Self { @@ -240,6 +252,7 @@ impl DataModelBuilder { pub fn build_and_set(self) { set_data_model(&ExecutorDataModel::new( self.permissions, + self.custom_instruction, data_model::JsonString::serialize(&self.schema) .expect("schema serialization must not fail"), ))