diff --git a/Cargo.lock b/Cargo.lock index 65016298195..c2cf4a8c71a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3826,6 +3826,7 @@ dependencies = [ "thiserror", "tokio", "vara-runtime", + "wasmi 0.30.0 (registry+https://github.com/rust-lang/crates.io-index)", "which", "whoami", ] diff --git a/gcli/Cargo.toml b/gcli/Cargo.toml index ba5b8806ba1..46d21f695c0 100644 --- a/gcli/Cargo.toml +++ b/gcli/Cargo.toml @@ -46,6 +46,8 @@ reqwest = { workspace = true, default-features = false, features = [ "json", "ru etc.workspace = true sp-io = { workspace = true, features = [ "std" ] } sp-core = { workspace = true, features = [ "std" ] } +# TODO: use wasmi from workspace (#3214) +wasmi = { version = "0.30.0", features = ["std"] } [dev-dependencies] rand.workspace = true diff --git a/gcli/src/meta/executor.rs b/gcli/src/meta/executor.rs new file mode 100644 index 00000000000..be76fd537c4 --- /dev/null +++ b/gcli/src/meta/executor.rs @@ -0,0 +1,312 @@ +// This file is part of Gear. +// +// Copyright (C) 2021-2023 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! The WASM executor in this module is just for parsing the state types +//! of gear programs, some of the host functions are missing logics that +//! is because they are for the on-chain environment data. + +use anyhow::{anyhow, Result}; +use wasmi::{AsContextMut, Engine, Extern, Linker, Memory, MemoryType, Module, Store}; + +const PAGE_STORAGE_PREFIX: [u8; 32] = *b"gcligcligcligcligcligcligcligcli"; + +/// HostState for the WASM executor +#[derive(Default)] +pub struct HostState { + /// Message buffer in host state. + pub msg: Vec, +} + +/// Call `metadata` method in the WASM code. +pub fn call_metadata(wasm: &[u8]) -> Result> { + execute(wasm, "metadata") +} + +/// Executes the WASM code. +fn execute(wasm: &[u8], method: &str) -> Result> { + assert!(gear_lazy_pages_interface::try_to_enable_lazy_pages( + PAGE_STORAGE_PREFIX + )); + + let engine = Engine::default(); + let module = Module::new(&engine, wasm).unwrap(); + + let mut store = Store::new(&engine, HostState::default()); + let mut linker = >::new(&engine); + let memory = Memory::new( + store.as_context_mut(), + MemoryType::new(256, None).map_err(|_| anyhow!("failed to create memory type"))?, + ) + .map_err(|_| anyhow!("failed to create memory"))?; + + // Execution environment + // + // TODO: refactor this after #3416. + { + let mut env = env::Env { + linker: &mut linker, + store: &mut store, + memory, + }; + + for import in module.imports() { + env.define(import.module(), import.name())?; + } + } + + let instance = linker + .instantiate(&mut store, &module) + .unwrap() + .start(&mut store)?; + + let metadata = instance + .get_export(&store, method) + .and_then(Extern::into_func) + .ok_or_else(|| anyhow!("could not find function \"{}\"", method))? + .typed::<(), ()>(&mut store)?; + + metadata.call(&mut store, ())?; + Ok(store.data().msg.clone()) +} + +mod env { + use super::HostState; + use anyhow::{anyhow, Result}; + use wasmi::{ + core::{Pages, Trap, TrapCode}, + AsContext, AsContextMut, Caller, Extern, Func, Linker, Memory, Store, + }; + + /// Environment for the wasm execution. + pub struct Env<'e> { + pub linker: &'e mut Linker, + pub store: &'e mut Store, + pub memory: Memory, + } + + macro_rules! func { + ($store:tt) => { + func!($store,) + }; + ($store:tt, $($ty:tt),* ) => { + Extern::Func(Func::wrap( + $store, + move |_caller: Caller<'_, HostState>, $(_: $ty),*| { Ok(()) }, + )) + }; + (@result $store:tt, $($ty:tt),* ) => { + Extern::Func(Func::wrap( + $store, + move |_caller: Caller<'_, HostState>, $(_: $ty),*| { 0 }, + )) + }; + } + + impl Env<'_> { + /// Define function in the environment. + pub fn define(&mut self, module: &str, name: &str) -> Result<()> { + if module != "env" { + return Err(anyhow!("module \"{}\" not found", module)); + } + + let memory = self.memory; + let store = &mut self.store; + + let external = match name { + "memory" => Extern::Memory(memory), + "alloc" => alloc(self.store, memory), + "gr_oom_panic" => gr_oom_panic(store), + "gr_read" => gr_read(store, memory), + "gr_reply" => gr_reply(store, memory), + "gr_panic" => gr_panic(store, memory), + "gr_size" => gr_size(store, memory), + // methods may be used by programs but not required by metadata. + "free" => func!(@result store, i32), + "gr_block_height" => func!(store, u32), + "gr_block_timestamp" => func!(store, u32), + "gr_create_program_wgas" => func!(store, i32, i32, u32, i32, u32, u64, u32, i32), + "gr_create_program" => func!(store, i32, i32, u32, i32, u32, u64, i32), + "gr_debug" => func!(store, i32, u32), + "gr_exit" => func!(store, i32), + "gr_gas_available" => func!(store, i32), + "gr_leave" => func!(store), + "gr_message_id" => func!(store, i32), + "gr_out_of_gas" => func!(store), + "gr_pay_program_rent" => func!(store, i32, i32), + "gr_program_id" => func!(store, i32), + "gr_random" => func!(store, i32, i32), + "gr_reply_code" => func!(store, i32), + "gr_reply_commit" => func!(store, i32, i32), + "gr_reply_deposit" => func!(store, i32, u64, i32), + "gr_reply_input" => func!(store, u32, u32, i32, i32), + "gr_reply_push" => func!(store, i32, u32, i32), + "gr_reply_push_input" => func!(store, u32, u32, i32), + "gr_reply_push_input_wgas" => func!(store, u32, u32, u64, i32, i32), + "gr_reply_to" => func!(store, i32), + "gr_reply_wgas" => func!(store, i32, u32, u64, i32, i32), + "gr_reservation_reply" => func!(store, i32, i32, u32, i32), + "gr_reservation_send_commit" => func!(store, u32, i32, u32, i32), + "gr_reservation_send" => func!(store, i32, i32, u32, u32, i32), + "gr_reserve_gas" => func!(store, u64, u32, i32), + "gr_send" => func!(store, i32, i32, u32, u32, i32), + "gr_send_commit" => func!(store, u32, i32, u32, i32), + "gr_send_commit_wgas" => func!(store, u32, i32, u64, u32, i32), + "gr_send_init" => func!(store, i32), + "gr_send_input" => func!(store, i32, u32, u32, u32, i32), + "gr_send_input_wgas" => func!(store, i32, u32, u32, u64, u32, i32), + "gr_send_push" => func!(store, u32, i32, u32, i32), + "gr_send_push_input" => func!(store, u32, u32, u32, i32), + "gr_send_wgas" => func!(store, i32, i32, u32, u64, u32, i32), + "gr_signal_code" => func!(store, i32), + "gr_signal_from" => func!(store, i32), + "gr_source" => func!(store, i32), + "gr_system_reserve_gas" => func!(store, u64, i32), + "gr_unreserve_gas" => func!(store, i32, i32), + "gr_value" => func!(store, i32), + "gr_wait" => func!(store, u32), + "gr_wait_for" => func!(store, u32), + "gr_wait_up_to" => func!(store, u32), + "gr_wake" => func!(store, i32, u32, i32), + _ => return Err(anyhow!("export \"{}\" not found in env", name,)), + }; + + self.linker.define(module, name, external)?; + + Ok(()) + } + } + + fn alloc(store: &mut Store, memory: Memory) -> Extern { + Extern::Func(Func::wrap( + store, + move |mut caller: Caller<'_, HostState>, pages: u32| { + memory + .clone() + .grow( + caller.as_context_mut(), + Pages::new(pages).unwrap_or_default(), + ) + .map_or_else( + |err| { + log::error!("{err:?}"); + u32::MAX as i32 + }, + |pages| pages.to_bytes().unwrap_or_default() as i32, + ) + }, + )) + } + + fn gr_read(ctx: &mut Store, memory: Memory) -> Extern { + Extern::Func(Func::wrap( + ctx, + move |mut caller: Caller<'_, HostState>, at: u32, len: i32, buff: i32, err: i32| { + let (at, len, buff, err) = (at as _, len as usize, buff as _, err as _); + + let msg = &caller.data().msg; + let payload = if at + len <= msg.len() { + msg[at..(at + len)].to_vec() + } else { + return Err(Trap::new(TrapCode::MemoryOutOfBounds.trap_message())); + }; + + let len: u32 = memory + .clone() + .write(caller.as_context_mut(), buff, &payload) + .map_err(|e| log::error!("{:?}", e)) + .is_err() + .into(); + + memory + .clone() + .write(caller.as_context_mut(), err, &len.to_le_bytes()) + .map_err(|e| { + log::error!("{:?}", e); + Trap::new(TrapCode::MemoryOutOfBounds.trap_message()) + })?; + + Ok(()) + }, + )) + } + + fn gr_reply(ctx: &mut Store, memory: Memory) -> Extern { + Extern::Func(Func::wrap( + ctx, + move |mut caller: Caller<'_, HostState>, ptr: u32, len: i32, _value: i32, _err: i32| { + let mut result = vec![0; len as usize]; + + memory + .read(caller.as_context(), ptr as usize, &mut result) + .map_err(|e| { + log::error!("{:?}", e); + Trap::new(TrapCode::MemoryOutOfBounds.trap_message()) + })?; + caller.data_mut().msg = result; + + Ok(()) + }, + )) + } + + fn gr_size(ctx: &mut Store, memory: Memory) -> Extern { + Extern::Func(Func::wrap( + ctx, + move |mut caller: Caller<'_, HostState>, size_ptr: u32| { + let size = caller.data().msg.len() as u32; + + memory + .clone() + .write( + caller.as_context_mut(), + size_ptr as usize, + &size.to_le_bytes(), + ) + .map_err(|e| { + log::error!("{:?}", e); + Trap::new(TrapCode::MemoryOutOfBounds.trap_message()) + })?; + + Ok(()) + }, + )) + } + + fn gr_panic(ctx: &mut Store, memory: Memory) -> Extern { + Extern::Func(Func::wrap( + ctx, + move |caller: Caller<'_, HostState>, ptr: u32, len: i32| { + let mut buff = Vec::with_capacity(len as usize); + memory.read(caller, ptr as usize, &mut buff).map_err(|e| { + log::error!("{e:?}"); + Trap::new(TrapCode::MemoryOutOfBounds.trap_message()) + })?; + + log::error!("Panic: {}", String::from_utf8_lossy(&buff)); + Ok(()) + }, + )) + } + + fn gr_oom_panic(ctx: impl AsContextMut) -> Extern { + Extern::Func(Func::wrap(ctx, || { + log::error!("OOM panic occurred"); + Ok(()) + })) + } +} diff --git a/gcli/src/meta/mod.rs b/gcli/src/meta/mod.rs index 4028cccb4a1..1a34dd2af0d 100644 --- a/gcli/src/meta/mod.rs +++ b/gcli/src/meta/mod.rs @@ -17,13 +17,13 @@ // along with this program. If not, see . //! Program metadata parser +mod executor; mod registry; #[cfg(test)] mod tests; use crate::result::{Error, Result}; -use core_processor::configs::BlockInfo; -use gear_core::code::{Code, CodeAndId, InstrumentedCode, InstrumentedCodeAndId, TryNewCodeConfig}; +use gear_core::code::{Code, CodeAndId, InstrumentedCodeAndId, TryNewCodeConfig}; use gmeta::{MetadataRepr, MetawasmData, TypesRepr}; use registry::LocalRegistry as _; use scale_info::{scale::Decode, PortableRegistry}; @@ -73,8 +73,6 @@ pub enum Meta { } impl Meta { - const PAGE_STORAGE_PREFIX: [u8; 32] = *b"gcligcligcligcligcligcligcligcli"; - fn format_metadata(meta: &MetadataRepr, fmt: &mut fmt::Formatter) -> fmt::Result { let registry = PortableRegistry::decode(&mut meta.registry.as_ref()).map_err(|_| fmt::Error)?; @@ -107,26 +105,6 @@ impl Meta { display.finish() } - /// Execute meta method. - fn execute(wasm: InstrumentedCode, method: &str) -> Result> { - assert!(gear_lazy_pages_interface::try_to_enable_lazy_pages( - Self::PAGE_STORAGE_PREFIX - )); - - sp_io::TestExternalities::default().execute_with(|| { - core_processor::informational::execute_for_reply::( - method.into(), - wasm, - None, - None, - Default::default(), - u64::MAX, - BlockInfo::default(), - ) - .map_err(Error::WasmExecution) - }) - } - /// Decode metawasm from wasm binary. pub fn decode_wasm(wasm: &[u8]) -> Result { let code = Code::try_new_mock_const_or_no_rules( @@ -135,10 +113,9 @@ impl Meta { TryNewCodeConfig::new_no_exports_check(), )?; let (code, _) = InstrumentedCodeAndId::from(CodeAndId::new(code)).into_parts(); + let result = executor::call_metadata(code.code())?; - Ok(Self::Wasm(MetawasmData::decode( - &mut Self::execute(code, "metadata")?.as_ref(), - )?)) + Ok(Self::Wasm(MetawasmData::decode(&mut result.as_ref())?)) } /// Decode metadata from hex bytes. diff --git a/gcli/src/result.rs b/gcli/src/result.rs index 8b6e2546509..621cd7e41ef 100644 --- a/gcli/src/result.rs +++ b/gcli/src/result.rs @@ -65,6 +65,8 @@ pub enum Error { InvalidWasm, #[error("Wasm execution error {0}")] WasmExecution(String), + #[error("Wasmi execution error {0}")] + Wasmi(wasmi::Error), #[error(transparent)] Etc(#[from] etc::Error), #[error("Metadata parsing error {0:?}")] diff --git a/gcli/tests/cmd/program.rs b/gcli/tests/cmd/program.rs index 46c83bbfb22..d1e1594afa1 100644 --- a/gcli/tests/cmd/program.rs +++ b/gcli/tests/cmd/program.rs @@ -97,9 +97,10 @@ Metadata { #[test] fn test_command_program_metadata_works() -> Result<()> { + let node = common::dev()?; let meta = env::wasm_bin("demo_new_meta.meta.txt"); let args = Args::new("program").action("meta").meta(meta); - let result = common::gcli(Vec::::from(args)).expect("run gcli failed"); + let result = node.run(args)?; let stdout = result.stdout.convert(); assert_eq!( @@ -113,13 +114,14 @@ fn test_command_program_metadata_works() -> Result<()> { #[test] fn test_command_program_metadata_derive_works() -> Result<()> { let meta = env::wasm_bin("demo_new_meta.meta.txt"); + let node = common::dev()?; let args = Args::new("program") .action("meta") .meta(meta) .flag("--derive") .derive("Person"); - let result = common::gcli(Vec::::from(args)).expect("run gcli failed"); + let result = node.run(args)?; let stdout = result.stdout.convert(); let expected = "Person { surname: String, name: String }"; @@ -153,9 +155,10 @@ Exports { #[test] fn test_command_program_metawasm_works() -> Result<()> { + let node = common::dev()?; let meta = env::wasm_bin("demo_meta_state_v1.meta.wasm"); let args = Args::new("program").action("meta").meta(meta); - let result = common::gcli(Vec::::from(args)).expect("run gcli failed"); + let result = node.run(args)?; let stdout = result.stdout.convert(); assert_eq!( @@ -168,6 +171,7 @@ fn test_command_program_metawasm_works() -> Result<()> { #[test] fn test_command_program_metawasm_derive_works() -> Result<()> { + let node = common::dev()?; let meta = env::wasm_bin("demo_meta_state_v1.meta.wasm"); let args = Args::new("program") .action("meta") @@ -175,7 +179,7 @@ fn test_command_program_metawasm_derive_works() -> Result<()> { .flag("--derive") .derive("Person"); - let result = common::gcli(Vec::::from(args)).expect("run gcli failed"); + let result = node.run(args)?; let stdout = result.stdout.convert(); let expected = "Person { surname: String, name: String }"; diff --git a/gcli/tests/cmd/send.rs b/gcli/tests/cmd/send.rs index a1933576f03..83d8fb2020c 100644 --- a/gcli/tests/cmd/send.rs +++ b/gcli/tests/cmd/send.rs @@ -36,6 +36,7 @@ async fn test_command_send_works() -> Result<()> { // Send message to messager let dest = hex::encode(mailbox[0].0.source.0); let _ = node.run(Args::new("send").destination(dest).gas_limit("2000000000"))?; + let mailbox = signer .api() .mailbox(Some(common::alice_account_id()), 10) diff --git a/gcli/tests/common/mod.rs b/gcli/tests/common/mod.rs index dbbafc92d1c..78494bb3d1b 100644 --- a/gcli/tests/common/mod.rs +++ b/gcli/tests/common/mod.rs @@ -20,7 +20,7 @@ pub use self::{ args::Args, result::{Error, Result}, - traits::NodeExec, + traits::{Convert, NodeExec}, }; use gear_core::ids::{CodeId, ProgramId}; use gsdk::{ @@ -60,13 +60,14 @@ impl NodeExec for Node { /// Run binary `gcli` pub fn gcli(args: impl IntoIterator) -> Result { - Ok(Command::new(env::bin("gcli")) + Command::new(env::bin("gcli")) .args( args.into_iter() .map(|v| v.to_string()) .collect::>(), ) - .output()?) + .output() + .map_err(Into::into) } /// Run the dev node diff --git a/gcli/tests/common/node.rs b/gcli/tests/common/node.rs deleted file mode 100644 index 8b137891791..00000000000 --- a/gcli/tests/common/node.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/gcli/tests/common/result.rs b/gcli/tests/common/result.rs index ca23e82a260..aa2a5ab642e 100644 --- a/gcli/tests/common/result.rs +++ b/gcli/tests/common/result.rs @@ -18,6 +18,8 @@ #[derive(Debug, thiserror::Error)] pub enum Error { + #[error(transparent)] + Anyhow(#[from] anyhow::Error), #[error(transparent)] GCli(#[from] gcli::result::Error), #[error(transparent)] diff --git a/gsdk/src/signer/utils.rs b/gsdk/src/signer/utils.rs index 759d2477c63..84a568ef556 100644 --- a/gsdk/src/signer/utils.rs +++ b/gsdk/src/signer/utils.rs @@ -196,6 +196,7 @@ impl Inner { call: Call, fields: impl Into>, ) -> Result { + log::info!("Run tx: {}::{}", Call::PALLET, call.call_name()); let tx = subxt::dynamic::tx(Call::PALLET, call.call_name(), fields.into()); self.process(tx).await