From 204163b4ab43b1efeb286fa5ce39ca6fb96304f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marin=20Ver=C5=A1i=C4=87?= Date: Thu, 27 Jan 2022 08:47:10 +0100 Subject: [PATCH] [feature] #1425: Wasm helper crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add helper crate for writing wasm smartcontracts Signed-off-by: Marin Veršić * add Execute trait, add CI workflow for wasm helper crate Signed-off-by: Marin Veršić --- .github/workflows/iroha2-dev-pr-static.yml | 10 +- .github/workflows/iroha2-dev-pr-unstable.yml | 3 + .github/workflows/iroha2-dev-pr-wasm.yaml | 44 ++++ .github/workflows/iroha2-dev-pr.yml | 13 +- .github/workflows/iroha2-dev.yml | 4 +- .github/workflows/iroha2-pr-heavy.yml | 3 + .gitignore | 1 + Dockerfile.ci | 4 +- core/src/smartcontracts/wasm.rs | 59 +++-- crypto/src/hash.rs | 2 +- crypto/src/signature.rs | 1 + data_model/src/lib.rs | 6 +- data_model/src/merkle.rs | 2 +- data_model/src/metadata.rs | 4 +- data_model/src/query.rs | 2 +- wasm/.cargo/config.toml | 5 + wasm/Cargo.toml | 28 +++ wasm/derive/Cargo.toml | 19 ++ wasm/derive/src/lib.rs | 77 ++++++ wasm/src/lib.rs | 240 +++++++++++++++++++ 20 files changed, 486 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/iroha2-dev-pr-wasm.yaml create mode 100644 wasm/.cargo/config.toml create mode 100644 wasm/Cargo.toml create mode 100644 wasm/derive/Cargo.toml create mode 100644 wasm/derive/src/lib.rs create mode 100644 wasm/src/lib.rs diff --git a/.github/workflows/iroha2-dev-pr-static.yml b/.github/workflows/iroha2-dev-pr-static.yml index 747be1dc764..23e9c290068 100644 --- a/.github/workflows/iroha2-dev-pr-static.yml +++ b/.github/workflows/iroha2-dev-pr-static.yml @@ -9,6 +9,9 @@ on: - "**.toml" - "**.yml" + # Not part of the workspace + - "!wasm/**" + jobs: check: runs-on: [self-hosted, Linux] @@ -17,20 +20,21 @@ jobs: steps: - uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v1 + - name: Format check run: cargo +nightly-2021-12-02 fmt --all -- --check - name: Static analysis without features run: cargo lints clippy --workspace --benches --tests --examples --quiet --no-default-features if: always() - name: Static analysis with all features enabled - run: cargo lints clippy --workspace --benches --tests --examples --all-features --quiet + run: cargo lints clippy --workspace --benches --tests --examples --quiet --all-features if: always() - - name: Verify iroha_data_model supports no_std + - name: Verify iroha_data_model still supports no_std run: cargo nono check --package iroha_data_model --no-default-features if: always() + - name: Documentation check run: | cargo doc --no-deps --quiet ./scripts/check_docs.sh if: always() - diff --git a/.github/workflows/iroha2-dev-pr-unstable.yml b/.github/workflows/iroha2-dev-pr-unstable.yml index cbf1be8a407..54454633d7d 100644 --- a/.github/workflows/iroha2-dev-pr-unstable.yml +++ b/.github/workflows/iroha2-dev-pr-unstable.yml @@ -9,6 +9,9 @@ on: - "**.toml" - "**.yml" + # Not part of the workspace + - "!wasm/**" + env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/iroha2-dev-pr-wasm.yaml b/.github/workflows/iroha2-dev-pr-wasm.yaml new file mode 100644 index 00000000000..e8947ff0f85 --- /dev/null +++ b/.github/workflows/iroha2-dev-pr-wasm.yaml @@ -0,0 +1,44 @@ +name: I2::Dev::Wasm + +defaults: + run: + working-directory: wasm + +on: + pull_request: + branches: [iroha2-dev] + paths: + - "wasm/**.rs" + - "wasm/**.json" + - "wasm/**.toml" + - "wasm/**.yml" + +env: + RUSTC_BOOTSTRAP: 1 + +jobs: + wasm32: + runs-on: [self-hosted, Linux] + container: + image: 7272721/i2-ci:latest + steps: + - uses: actions/checkout@v2 + - uses: Swatinem/rust-cache@v1 + + # Static analysis + - name: Format check + run: cargo +nightly-2021-12-02 fmt --all -- --check + - name: Static analysis without features + run: cargo lints clippy --benches --tests --examples --quiet --no-default-features + if: always() + - name: Static analysis with all features enabled + run: cargo lints clippy --benches --tests --examples --quiet --all-features + if: always() + + - name: Verify iroha_wasm still supports no_std + run: cargo nono check --package iroha_wasm + if: always() + + # Tests + - name: Run tests + run: mold -run cargo test --tests --quiet --no-fail-fast diff --git a/.github/workflows/iroha2-dev-pr.yml b/.github/workflows/iroha2-dev-pr.yml index 024bfddd142..6a91f2afba6 100644 --- a/.github/workflows/iroha2-dev-pr.yml +++ b/.github/workflows/iroha2-dev-pr.yml @@ -9,8 +9,12 @@ on: - "**.toml" - "**.yml" + # Not part of the workspace + - "!wasm/**" + env: CARGO_TERM_COLOR: always + RUSTC_BOOTSTRAP: 1 jobs: test: @@ -23,19 +27,13 @@ jobs: - uses: Swatinem/rust-cache@v1 - name: Run tests run: mold -run cargo test --quiet --workspace --no-fail-fast -- --skip unstable_network --test-threads 2 - env: - RUSTC_BOOTSTRAP: 1 - name: Run iroha tests with network mock run: mold -run cargo test --quiet --features mock -- --ignored --skip unstable_network --test-threads 2 - env: - RUSTC_BOOTSTRAP: 1 working-directory: core/test_network - name: Run iroha_actor deadlock detection tests + working-directory: actor run: mold -run cargo test --quiet --features deadlock_detection -- --skip unstable_network --test-threads 2 if: always() - env: - RUSTC_BOOTSTRAP: 1 - working-directory: actor # Coverage is both in PR and in push pipelines so that: # 1. PR can get coverage report from bot. @@ -51,7 +49,6 @@ jobs: run: mold -run cargo test --quiet --workspace --no-fail-fast -- --skip unstable_network --test-threads 2 || true env: RUSTFLAGS: "-Zinstrument-coverage" - RUSTC_BOOTSTRAP: 1 LLVM_PROFILE_FILE: "iroha-%p-%m.profraw" - name: Generate a grcov coverage report run: grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore "/*" -o lcov.info diff --git a/.github/workflows/iroha2-dev.yml b/.github/workflows/iroha2-dev.yml index b9382720968..4f83dd320ae 100644 --- a/.github/workflows/iroha2-dev.yml +++ b/.github/workflows/iroha2-dev.yml @@ -171,7 +171,7 @@ jobs: # Coverage is both in PR and in push pipelines so that: # 1. PR can get coverage report from bot. - # 2 Coverage bot can have results from `iroha2-dev` to report coverage changes. + # 2. Coverage bot can have results from `iroha2-dev` to report coverage changes. coverage: runs-on: ubuntu-latest container: @@ -180,7 +180,7 @@ jobs: - uses: actions/checkout@v2 - uses: Swatinem/rust-cache@v1 - name: Run tests - run: mold -run cargo test --workspace --no-fail-fast -- --skip unstable_network || true + run: mold -run cargo test --workspace --no-fail-fast -- --skip unstable_network || true env: RUSTFLAGS: "-Zinstrument-coverage" RUSTC_BOOTSTRAP: 1 diff --git a/.github/workflows/iroha2-pr-heavy.yml b/.github/workflows/iroha2-pr-heavy.yml index 57abb1197f5..3ab432b2da0 100644 --- a/.github/workflows/iroha2-pr-heavy.yml +++ b/.github/workflows/iroha2-pr-heavy.yml @@ -11,6 +11,9 @@ on: - "**.toml" - "**.yml" + # Not part of the workspace + - "!wasm/**" + env: CARGO_TERM_COLOR: always diff --git a/.gitignore b/.gitignore index 7e04d9ba4bb..a96c5f63064 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ **/blocks/ **/*.rs.bk **/rusty-tags.vi +/**/Cargo.lock config/__pycaсhe__ **/__pycache__/* diff --git a/Dockerfile.ci b/Dockerfile.ci index 25f20993652..66e76cff01a 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -49,8 +49,9 @@ RUN git clone https://github.com/rui314/mold.git; \ rm -rf "./mold" RUN rustup component add llvm-tools-preview clippy; \ + rustup target add wasm32-unknown-unknown; \ rustup install --profile default nightly-2021-12-02; \ - cargo install cargo-lints cargo-nono grcov + cargo install cargo-lints cargo-nono webassembly-test-runner grcov RUN curl -fsSL https://get.docker.com -o get-docker.sh; \ chmod +x get-docker.sh; \ @@ -62,4 +63,3 @@ RUN curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-c chmod +x /usr/local/bin/docker-compose # Refer to docker-compose.yml to see which ports should be exposed. - diff --git a/core/src/smartcontracts/wasm.rs b/core/src/smartcontracts/wasm.rs index f47e6077bf7..2bd31ca49cb 100644 --- a/core/src/smartcontracts/wasm.rs +++ b/core/src/smartcontracts/wasm.rs @@ -13,10 +13,12 @@ use crate::{ wsv::{WorldStateView, WorldTrait}, }; -const WASM_ALLOC_FN: &str = "alloc"; +type WasmUsize = u32; + +const WASM_ALLOC_FN: &str = "_iroha_wasm_alloc"; const WASM_MEMORY_NAME: &str = "memory"; -const WASM_MAIN_FN_NAME: &str = "main"; -const EXECUTE_ISI_FN_NAME: &str = "execute_isi"; +const WASM_MAIN_FN_NAME: &str = "_iroha_wasm_main"; +const EXECUTE_ISI_FN_NAME: &str = "execute_instruction"; const EXECUTE_QUERY_FN_NAME: &str = "execute_query"; /// `WebAssembly` execution error type @@ -33,7 +35,7 @@ pub enum Error { ExportNotFound(#[source] anyhow::Error), /// Call to function exported from module failed #[error("Exported function call failed")] - ExportFnCall(#[source] Trap), + ExportFnCall(#[from] Trap), /// Some other error happened #[error(transparent)] Other(eyre::Error), @@ -130,14 +132,19 @@ impl<'a, W: WorldTrait> Runtime<'a, W> { /// Host defined function which executes query. When calling this function, module /// serializes query to linear memory and provides offset and length as parameters /// + /// # Warning + /// + /// This function doesn't take ownership of the provided allocation + /// but it does transfer ownership of the result to the caller + /// /// # Errors /// /// If decoding or execution of the query fails fn execute_query( mut caller: Caller>, - offset: u32, - len: u32, - ) -> Result<(u32, u32), Trap> { + offset: WasmUsize, + len: WasmUsize, + ) -> Result<(WasmUsize, WasmUsize), Trap> { let alloc_fn = Self::get_alloc_fn(&mut caller)?; let memory = Self::get_memory(&mut caller)?; @@ -152,8 +159,8 @@ impl<'a, W: WorldTrait> Runtime<'a, W> { .map_err(|e| Trap::new(e.to_string()))? .encode(); - let res_bytes_len: u32 = { - let res_bytes_len: Result = res_bytes.len().try_into(); + let res_bytes_len: WasmUsize = { + let res_bytes_len: Result = res_bytes.len().try_into(); res_bytes_len.map_err(|error| Trap::new(error.to_string()))? }; @@ -174,10 +181,19 @@ impl<'a, W: WorldTrait> Runtime<'a, W> { /// Host defined function which executes ISI. When calling this function, module /// serializes ISI to linear memory and provides offset and length as parameters /// + /// # Warning + /// + /// This function doesn't take ownership of the provided allocation + /// but it does tranasfer ownership of the result to the caller + /// /// # Errors /// /// If decoding or execution of the ISI fails - fn execute_isi(mut caller: Caller>, offset: u32, len: u32) -> Result<(), Trap> { + fn execute_instruction( + mut caller: Caller>, + offset: WasmUsize, + len: WasmUsize, + ) -> Result<(), Trap> { let memory = Self::get_memory(&mut caller)?; // Accessing memory as a byte slice to avoid the use of unsafe @@ -200,7 +216,7 @@ impl<'a, W: WorldTrait> Runtime<'a, W> { let mut linker = Linker::new(engine); linker - .func_wrap("iroha", EXECUTE_ISI_FN_NAME, Self::execute_isi) + .func_wrap("iroha", EXECUTE_ISI_FN_NAME, Self::execute_instruction) .map_err(Error::Initialization)?; linker @@ -210,13 +226,15 @@ impl<'a, W: WorldTrait> Runtime<'a, W> { Ok(linker) } - fn get_alloc_fn(caller: &mut Caller>) -> Result, Trap> { + fn get_alloc_fn( + caller: &mut Caller>, + ) -> Result, Trap> { caller .get_export(WASM_ALLOC_FN) .ok_or_else(|| Trap::new(format!("{}: export not found", WASM_ALLOC_FN)))? .into_func() .ok_or_else(|| Trap::new(format!("{}: not a function", WASM_ALLOC_FN)))? - .typed::(caller) + .typed::(caller) .map_err(|_error| Trap::new(format!("{}: unexpected declaration", WASM_ALLOC_FN))) } @@ -257,7 +275,7 @@ impl<'a, W: WorldTrait> Runtime<'a, W> { .instantiate(&mut store, &module) .map_err(Error::Instantiation)?; let alloc_fn = instance - .get_typed_func::(&mut store, WASM_ALLOC_FN) + .get_typed_func::(&mut store, WASM_ALLOC_FN) .map_err(Error::ExportNotFound)?; let memory = instance @@ -272,7 +290,10 @@ impl<'a, W: WorldTrait> Runtime<'a, W> { let account_bytes_len = account_bytes .len() .try_into() - .wrap_err("Scale encoded account ID has size larger than u32::MAX") + .wrap_err(format!( + "Encoded account ID has size larger than {}::MAX", + std::any::type_name::() + )) .map_err(Error::Other)?; let account_offset = { @@ -286,11 +307,13 @@ impl<'a, W: WorldTrait> Runtime<'a, W> { acc_offset }; - let main = instance - .get_typed_func::<(u32, u32), (), _>(&mut store, WASM_MAIN_FN_NAME) + let main_fn = instance + .get_typed_func::<(WasmUsize, WasmUsize), (), _>(&mut store, WASM_MAIN_FN_NAME) .map_err(Error::ExportNotFound)?; - main.call(&mut store, (account_offset, account_bytes_len)) + // NOTE: This function takes ownership of the pointer + main_fn + .call(&mut store, (account_offset, account_bytes_len)) .map_err(Error::ExportFnCall)?; Ok(()) diff --git a/crypto/src/hash.rs b/crypto/src/hash.rs index 937303de4e7..f8df42f8cc5 100644 --- a/crypto/src/hash.rs +++ b/crypto/src/hash.rs @@ -1,5 +1,5 @@ #[cfg(not(feature = "std"))] -use alloc::{format, string::String, vec::Vec}; +use alloc::{format, string::String, vec, vec::Vec}; use core::{ fmt::{self, Debug, Display, Formatter}, hash, diff --git a/crypto/src/signature.rs b/crypto/src/signature.rs index 9aebaabc80a..5fab051a7c2 100644 --- a/crypto/src/signature.rs +++ b/crypto/src/signature.rs @@ -4,6 +4,7 @@ use alloc::{ collections::{btree_map, btree_set}, format, string::String, + vec, vec::Vec, }; use core::{fmt, marker::PhantomData}; diff --git a/data_model/src/lib.rs b/data_model/src/lib.rs index 5230d1bf4a3..e83ef657302 100644 --- a/data_model/src/lib.rs +++ b/data_model/src/lib.rs @@ -1100,7 +1100,7 @@ pub mod asset { //! instructions implementations. #[cfg(not(feature = "std"))] - use alloc::{collections::btree_map, string::String, vec::Vec}; + use alloc::{collections::btree_map, format, string::String, vec::Vec}; use core::{ cmp::Ordering, fmt::{self, Display, Formatter}, @@ -1633,7 +1633,7 @@ pub mod domain { //! This module contains [`Domain`](`crate::domain::Domain`) structure and related implementations and trait implementations. #[cfg(not(feature = "std"))] - use alloc::{collections::btree_map, string::String, vec::Vec}; + use alloc::{collections::btree_map, format, string::String, vec::Vec}; use core::{cmp::Ordering, fmt, str::FromStr}; #[cfg(feature = "std")] use std::collections::btree_map; @@ -1823,7 +1823,7 @@ pub mod peer { //! This module contains [`Peer`] structure and related implementations and traits implementations. #[cfg(not(feature = "std"))] - use alloc::{string::String, vec::Vec}; + use alloc::{format, string::String, vec::Vec}; use core::{ cmp::Ordering, fmt, diff --git a/data_model/src/merkle.rs b/data_model/src/merkle.rs index 6e336c37d66..9d049039204 100644 --- a/data_model/src/merkle.rs +++ b/data_model/src/merkle.rs @@ -1,7 +1,7 @@ //! Merkle tree implementation #[cfg(not(feature = "std"))] -use alloc::{boxed::Box, vec, vec::Vec}; +use alloc::{boxed::Box, format, string::String, vec, vec::Vec}; #[cfg(feature = "std")] use std::collections::VecDeque; diff --git a/data_model/src/metadata.rs b/data_model/src/metadata.rs index aa91922ab49..2e2bb266d4a 100644 --- a/data_model/src/metadata.rs +++ b/data_model/src/metadata.rs @@ -53,7 +53,7 @@ pub enum Error { }, /// Empty path #[display(fmt = "Path specification empty")] - EmptyPath(), + EmptyPath, /// Middle path segment is missing. I.e. nothing was found at that key #[display(fmt = "{}: path segment not found", _0)] MissingSegment(Name), @@ -165,7 +165,7 @@ impl Metadata { actual: self.map.len(), }); } - let key = path.last().ok_or_else(Error::EmptyPath)?; + let key = path.last().ok_or(Error::EmptyPath)?; let mut layer = self; for k in path.iter().take(path.len() - 1) { layer = match layer diff --git a/data_model/src/query.rs b/data_model/src/query.rs index 07234dbfe3c..9921c32b470 100644 --- a/data_model/src/query.rs +++ b/data_model/src/query.rs @@ -145,7 +145,7 @@ declare_versioned_with_scale!(VersionedQueryResult 1..2, Debug, Clone, iroha_mac /// Sized container for all possible Query results. #[version_with_scale(n = 1, versioned = "VersionedQueryResult")] -#[derive(Debug, Clone, Decode, Encode, Deserialize, Serialize, IntoSchema)] +#[derive(Debug, Clone, PartialEq, Eq, Decode, Encode, Deserialize, Serialize, IntoSchema)] pub struct QueryResult(pub Value); #[cfg(all(feature = "std", feature = "warp"))] diff --git a/wasm/.cargo/config.toml b/wasm/.cargo/config.toml new file mode 100644 index 00000000000..00ec8ee28dc --- /dev/null +++ b/wasm/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +target = "wasm32-unknown-unknown" + +[target.wasm32-unknown-unknown] +runner = "webassembly-test-runner" diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml new file mode 100644 index 00000000000..041b211a482 --- /dev/null +++ b/wasm/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "iroha_wasm" +version = "0.1.0" +authors = ["Iroha 2 team "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +[workspace] +members = [ + "derive", +] + +[dependencies] +iroha_data_model = { version = "=2.0.0-pre.1", path = "../data_model", default-features = false } +iroha_wasm_derive = { path = "derive" } + +parity-scale-codec = { version = "2.3.1", default-features = false } +wee_alloc = "0.4.5" + +[dev-dependencies] +webassembly-test = "0.1.0" diff --git a/wasm/derive/Cargo.toml b/wasm/derive/Cargo.toml new file mode 100644 index 00000000000..6af4f73751b --- /dev/null +++ b/wasm/derive/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "iroha_wasm_derive" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +syn = {version = "1", default-features = false } +quote = "1.0" +proc-macro-error = "1.0" + +[dev-dependencies] +iroha_wasm = { path = "../" } + +trybuild = "1.0.53" diff --git a/wasm/derive/src/lib.rs b/wasm/derive/src/lib.rs new file mode 100644 index 00000000000..6d236229694 --- /dev/null +++ b/wasm/derive/src/lib.rs @@ -0,0 +1,77 @@ +//! Macros for writing smartcontracts + +#![allow(clippy::str_to_string)] + +use proc_macro::TokenStream; +use proc_macro_error::{abort, proc_macro_error}; +use quote::quote; +use syn::{parse_macro_input, parse_quote, ItemFn, Path, ReturnType, Signature, Type}; + +/// Used to annotate user-defined function which starts the execution of smartcontract +#[proc_macro_error] +#[proc_macro_attribute] +pub fn iroha_wasm(_: TokenStream, item: TokenStream) -> TokenStream { + let ItemFn { + attrs, + vis, + sig, + mut block, + }: ItemFn = parse_macro_input!(item as ItemFn); + + verify_function_signature(&sig); + let fn_name = &sig.ident; + + block.stmts.insert( + 0, + parse_quote!( + use iroha_wasm::Execute as _; + ), + ); + + quote! { + #[no_mangle] + unsafe extern "C" fn _iroha_wasm_main(ptr: u32, len: u32) { + #fn_name(iroha_wasm::_decode_from_raw::(ptr, len)) + } + + #(#attrs)* + #vis #sig + #block + } + .into() +} + +fn verify_function_signature(sig: &Signature) -> bool { + if ReturnType::Default != sig.output { + abort!(sig.output, "Exported function must not have a return type"); + } + + if sig.inputs.len() != 1 { + abort!( + sig.inputs, + "Exported function must have exactly 1 input argument of type `AccountId`" + ); + } + + if let Some(syn::FnArg::Typed(pat)) = sig.inputs.iter().next() { + if let syn::Type::Reference(ty) = &*pat.ty { + return type_is_account_id(&ty.elem); + } + } + + false +} + +fn type_is_account_id(account_id_ty: &Type) -> bool { + const ACCOUNT_ID_IDENT: &str = "AccountId"; + + if let Type::Path(path) = account_id_ty { + let Path { segments, .. } = &path.path; + + if let Some(type_name) = segments.iter().last().map(|ty| &ty.ident) { + return *type_name == ACCOUNT_ID_IDENT; + } + } + + false +} diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs new file mode 100644 index 00000000000..30107001a5f --- /dev/null +++ b/wasm/src/lib.rs @@ -0,0 +1,240 @@ +//! API which simplifies writing of smartcontracts + +#![feature(alloc_error_handler)] +// Required because of `unsafe` code and `no_mangle` use +#![allow(unsafe_code)] +#![no_std] + +#[cfg(all(not(test), not(target_pointer_width = "32")))] +compile_error!("Target architectures other then 32-bit are not supported"); + +#[cfg(all(not(test), not(all(target_arch = "wasm32", target_os = "unknown"))))] +compile_error!("Targets other then wasm32-unknown-unknown are not supported"); + +extern crate alloc; + +use alloc::{boxed::Box, format, vec::Vec}; + +use data_model::prelude::*; +pub use iroha_data_model as data_model; +pub use iroha_wasm_derive::iroha_wasm; +use parity_scale_codec::{Decode, Encode}; + +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[cfg(target_pointer_width = "32")] +type WasmUsize = u32; +#[cfg(target_pointer_width = "64")] +type WasmUsize = u64; + +#[no_mangle] +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &::core::panic::PanicInfo) -> ! { + // Need to provide a tiny `panic` implementation for `#![no_std]`. + // This translates into an `unreachable` instruction that will + // raise a `trap` the WebAssembly execution if we panic at runtime. + unreachable!("Program should have aborted") +} + +#[no_mangle] +#[cfg(not(test))] +#[alloc_error_handler] +fn oom(layout: ::core::alloc::Layout) -> ! { + panic!("Allocation({} bytes) failed", layout.size()) +} + +pub trait Execute { + type Result; + fn execute(&self) -> Self::Result; +} + +impl Execute for data_model::isi::Instruction { + type Result = (); + + /// Execute the given instruction on the host environment + fn execute(&self) -> Self::Result { + #[cfg(not(test))] + use host::execute_instruction as host_execute_instruction; + #[cfg(test)] + use tests::_iroha_wasm_execute_instruction_mock as host_execute_instruction; + + // Safety: `host_execute_instruction` doesn't take ownership of it's pointer parameter + unsafe { encode_and_execute(self, host_execute_instruction) }; + } +} +impl Execute for data_model::query::QueryBox { + type Result = QueryResult; + + /// Executes the given query on the host environment + fn execute(&self) -> Self::Result { + #[cfg(not(test))] + use host::execute_query as host_execute_query; + #[cfg(test)] + use tests::_iroha_wasm_execute_query_mock as host_execute_query; + + // Safety: - `host_execute_query` doesn't take ownership of it's pointer parameter + // - ownership of the returned result is transfered into `_decode_from_raw` + unsafe { + let host::WasmQueryResult(res_ptr, res_len) = + encode_and_execute(self, host_execute_query); + _decode_from_raw(res_ptr, res_len) + } + } +} + +#[no_mangle] +// `WasmUsize` is always pointer sized +#[allow(clippy::cast_possible_truncation)] +extern "C" fn _iroha_wasm_alloc(len: WasmUsize) -> WasmUsize { + core::mem::ManuallyDrop::new(Vec::::with_capacity(len as usize)).as_mut_ptr() as WasmUsize +} + +/// Host exports +mod host { + use super::WasmUsize; + + /// Helper struct which guarantees to be FFI safe since tuple is not + #[repr(C)] + #[must_use] + #[derive(Debug, Clone, Copy)] + pub(super) struct WasmQueryResult(pub WasmUsize, pub WasmUsize); + + #[link(wasm_import_module = "iroha")] + extern "C" { + /// Executes encoded query by providing offset and length + /// into WebAssembly's linear memory where query is stored + /// + /// # Warning + /// + /// This function doesn't take ownership of the provided allocation + /// but it does transfer ownership of the result to the caller + #[cfg(not(test))] + pub(super) fn execute_query(ptr: WasmUsize, len: WasmUsize) -> WasmQueryResult; + + /// Executes encoded instruction by providing offset and length + /// into WebAssembly's linear memory where instruction is stored + /// + /// # Warning + /// + /// This function doesn't take ownership of the provided allocation + /// but it does transfer ownership of the result to the caller + #[cfg(not(test))] + pub(super) fn execute_instruction(ptr: WasmUsize, len: WasmUsize); + } +} + +/// Decode the object from given pointer and length +/// +/// # Warning +/// +/// This method takes ownership of the given pointer +/// +/// # Safety +/// +/// It's safe to call this function as long as it's safe to construct `Box<[u8]>` from the given pointer +// `WasmUsize` is always pointer sized +#[allow(clippy::cast_possible_truncation)] +pub unsafe fn _decode_from_raw(ptr: WasmUsize, len: WasmUsize) -> T { + let bytes = Box::from_raw(core::slice::from_raw_parts_mut(ptr as *mut _, len as usize)); + + #[allow(clippy::expect_used, clippy::expect_fun_call)] + T::decode(&mut &bytes[..]).expect( + format!( + "Decoding of {} failed. This is a bug", + core::any::type_name::() + ) + .as_str(), + ) +} + +/// Encode the given object and call the given function with the pointer and length of the allocation +/// +/// # Warning +/// +/// Ownership of the returned allocation is transfered to the caller +/// +/// # Safety +/// +/// The given function must not take ownership of the pointer argument +unsafe fn encode_and_execute( + obj: &T, + fun: unsafe extern "C" fn(WasmUsize, WasmUsize) -> O, +) -> O { + // NOTE: It's imperative that encoded object is stored on the heap + // because heap corresponds to linear memory when compiled to wasm + let bytes = obj.encode(); + + // `WasmUsize` is always pointer sized + #[allow(clippy::cast_possible_truncation)] + let ptr = bytes.as_ptr() as WasmUsize; + // `WasmUsize` is always pointer sized + #[allow(clippy::cast_possible_truncation)] + let len = bytes.len() as WasmUsize; + + fun(ptr, len) +} + +/// Most used items +pub mod prelude { + pub use crate::{iroha_wasm, Execute}; +} + +#[cfg(test)] +mod tests { + #![allow(clippy::restriction)] + #![allow(clippy::pedantic)] + + use core::{mem::ManuallyDrop, slice}; + + use webassembly_test::webassembly_test; + + use super::*; + + const QUERY_RESULT: QueryResult = QueryResult(Value::U32(1234)); + + fn get_test_instruction() -> Instruction { + let new_account_id = AccountId::test("mad_hatter", "wonderland"); + let register_isi = RegisterBox::new(NewAccount::new(new_account_id)); + + Instruction::Register(register_isi) + } + fn get_test_query() -> QueryBox { + let account_id = AccountId::test("alice", "wonderland"); + FindAccountById::new(account_id).into() + } + + #[no_mangle] + pub(super) unsafe extern "C" fn _iroha_wasm_execute_instruction_mock( + ptr: WasmUsize, + len: WasmUsize, + ) { + let bytes = slice::from_raw_parts(ptr as *const _, len as usize); + let instruction = Instruction::decode(&mut &*bytes); + assert_eq!(get_test_instruction(), instruction.unwrap()); + } + + #[no_mangle] + pub(super) unsafe extern "C" fn _iroha_wasm_execute_query_mock( + ptr: WasmUsize, + len: WasmUsize, + ) -> host::WasmQueryResult { + let bytes = slice::from_raw_parts(ptr as *const _, len as usize); + let query = QueryBox::decode(&mut &*bytes).unwrap(); + assert_eq!(query, get_test_query()); + + let bytes = ManuallyDrop::new(QUERY_RESULT.encode().into_boxed_slice()); + host::WasmQueryResult(bytes.as_ptr() as WasmUsize, bytes.len() as WasmUsize) + } + + #[webassembly_test] + fn execute_instruction_test() { + get_test_instruction().execute() + } + + #[webassembly_test] + fn execute_query_test() { + assert_eq!(get_test_query().execute(), QUERY_RESULT); + } +}