wasm-runtime

A wasm runtime
git clone https://git.parazyd.org/wasm-runtime
Log | Files | Refs | README

commit e4fae9954d20225831248af32972253a3577d25f
Author: parazyd <parazyd@dyne.org>
Date:   Tue,  8 Mar 2022 22:58:49 +0100

Code import

Diffstat:
A.gitignore | 4++++
ACargo.toml | 32++++++++++++++++++++++++++++++++
AMakefile | 20++++++++++++++++++++
AREADME.md | 12++++++++++++
Adrk-sdk/Cargo.toml | 7+++++++
Adrk-sdk/src/entrypoint.rs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adrk-sdk/src/error.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Adrk-sdk/src/lib.rs | 2++
Arustfmt.toml | 9+++++++++
Asmart-contract/.gitignore | 3+++
Asmart-contract/Cargo.toml | 16++++++++++++++++
Asmart-contract/src/lib.rs | 23+++++++++++++++++++++++
Asrc/example.rs | 17+++++++++++++++++
Asrc/lib.rs | 3+++
Asrc/memory.rs | 41+++++++++++++++++++++++++++++++++++++++++
Asrc/runtime.rs | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util.rs | 10++++++++++
17 files changed, 397 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,4 @@ +/target +/wabt +Cargo.lock +smart_contract.wasm diff --git a/Cargo.toml b/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "wasm-runtime" +version = "0.1.0" +edition = "2021" + +[workspace] +members = ["drk-sdk"] +exclude = ["smart-contract"] + +#[profile.release] +#lto = true +#codegen-units = 1 +#overflow-checks = true + +[dependencies] +anyhow = "1.0.55" +wasmer = "2.2.0" +drk-sdk = { path = "./drk-sdk" } + +[dev-dependencies] +borsh = "0.9.3" +smart-contract = { path = "./smart-contract" } +#wasmer = "^2.0.0" + +[dev-dependencies.pasta_curves] +git = "https://github.com/parazyd/pasta_curves" +branch = "optional-borsh-support" +features = ["borsh"] + +[[example]] +name = "runner" +path = "src/example.rs" diff --git a/Makefile b/Makefile @@ -0,0 +1,20 @@ +SRC = \ + $(shell find src -type f) \ + $(shell find smart-contract -type f) \ + $(shell find drk-sdk -type f) + +CARGO = cargo + +DEPS = smart_contract.wasm + +all: $(DEPS) + $(CARGO) run --release --example runner + +wabt: + git clone --recursive https://github.com/WebAssembly/wabt $@ + $(MAKE) -C $@ + +smart_contract.wasm: wabt $(SRC) + cd smart-contract && $(CARGO) build --release --lib --target wasm32-unknown-unknown + cp -f smart-contract/target/wasm32-unknown-unknown/release/$@ $@ + ./wabt/bin/wasm-strip $@ diff --git a/README.md b/README.md @@ -0,0 +1,12 @@ +wasm-runtime +============ + +Experiments with WASM runtime. + +``` +$ make +``` + +* Smart contract is in `smart-contract/src/lib.rs` +* Contract helpers are in `drk-sdk/src` +* wasm runtime is in `src` diff --git a/drk-sdk/Cargo.toml b/drk-sdk/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "drk-sdk" +version = "0.1.0" +edition = "2021" + +[dependencies] +thiserror = "1.0.30" diff --git a/drk-sdk/src/entrypoint.rs b/drk-sdk/src/entrypoint.rs @@ -0,0 +1,59 @@ +use std::{mem::size_of, slice::from_raw_parts}; + +/// Success exit code for contract +pub const SUCCESS: u64 = 0; + +/// This macro is used to flag the contract entrypoint function. +/// All contracts must provide such a function and accept a payload. +/// +/// The payload is a slice of u8 prepended with a little-endian u64 +/// that tells the slice's length. +#[macro_export] +macro_rules! entrypoint { + ($process_instruction:ident) => { + /// # Safety + #[no_mangle] + pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 { + let instruction_data = $crate::entrypoint::deserialize(input); + + match $process_instruction(&instruction_data) { + Ok(()) => $crate::entrypoint::SUCCESS, + Err(e) => e.into(), + } + } + }; +} + +/// Deserialize a given payload in `entrypoint` +/// # Safety +pub unsafe fn deserialize<'a>(input: *mut u8) -> &'a [u8] { + let mut offset: usize = 0; + + let instruction_data_len = *(input.add(offset) as *const u64) as usize; + offset += size_of::<u64>(); + + let instruction_data = { from_raw_parts(input.add(offset), instruction_data_len) }; + + instruction_data +} + +/// Allocate a piece of memory in the wasm VM +#[no_mangle] +pub extern "C" fn __drkruntime_mem_alloc(size: usize) -> *mut u8 { + let align = std::mem::align_of::<usize>(); + + if let Ok(layout) = std::alloc::Layout::from_size_align(size, align) { + unsafe { + if layout.size() > 0 { + let ptr = std::alloc::alloc(layout); + if !ptr.is_null() { + return ptr + } + } else { + return align as *mut u8 + } + } + } + + std::process::abort(); +} diff --git a/drk-sdk/src/error.rs b/drk-sdk/src/error.rs @@ -0,0 +1,51 @@ +use std::result::Result as ResultGeneric; + +pub type ContractResult = ResultGeneric<(), ContractError>; + +#[derive(Debug, thiserror::Error)] +#[repr(C)] +pub enum ContractError { + #[error("Internal error")] + Internal, + + #[error("Out of memory")] + OutOfMemory, + + #[error("IO error")] + Io, + + #[error("Custom contract error: {0:#x}")] + Custom(u32), +} + +pub const INTERNAL_ERROR: u64 = 1; +pub const OUT_OF_MEMORY: u64 = 2; +pub const IO_ERROR: u64 = 3; + +impl From<ContractError> for u64 { + fn from(err: ContractError) -> Self { + match err { + ContractError::Internal => INTERNAL_ERROR, + ContractError::OutOfMemory => OUT_OF_MEMORY, + ContractError::Io => IO_ERROR, + ContractError::Custom(e) => e.into(), + } + } +} + +impl From<u64> for ContractError { + fn from(err: u64) -> Self { + match err { + INTERNAL_ERROR => ContractError::Internal, + OUT_OF_MEMORY => ContractError::OutOfMemory, + IO_ERROR => ContractError::Io, + e => ContractError::Custom(e.try_into().unwrap()), + } + } +} + +impl From<std::io::Error> for ContractError { + fn from(_: std::io::Error) -> Self { + Self::Io + } +} diff --git a/drk-sdk/src/lib.rs b/drk-sdk/src/lib.rs @@ -0,0 +1,2 @@ +pub mod entrypoint; +pub mod error; diff --git a/rustfmt.toml b/rustfmt.toml @@ -0,0 +1,9 @@ +reorder_imports = true +imports_granularity = "Crate" +use_small_heuristics = "Max" +comment_width = 100 +wrap_comments = false +binop_separator = "Back" +trailing_comma = "Vertical" +trailing_semicolon = false +use_field_init_shorthand = true diff --git a/smart-contract/.gitignore b/smart-contract/.gitignore @@ -0,0 +1,3 @@ +/target +/wabt +Cargo.lock diff --git a/smart-contract/Cargo.toml b/smart-contract/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "smart-contract" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +borsh = "0.9.3" +drk-sdk = { path = "../drk-sdk" } + +[dependencies.pasta_curves] +git = "https://github.com/parazyd/pasta_curves" +branch = "optional-borsh-support" +features = ["borsh"] diff --git a/smart-contract/src/lib.rs b/smart-contract/src/lib.rs @@ -0,0 +1,23 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use drk_sdk::{ + entrypoint, + error::{ContractError, ContractResult}, +}; +use pasta_curves::pallas; + +#[derive(BorshSerialize, BorshDeserialize)] +pub struct Args { + pub a: pallas::Base, + pub b: pallas::Base, +} + +entrypoint!(process_instruction); +fn process_instruction(ix: &[u8]) -> ContractResult { + let args = Args::try_from_slice(ix)?; + + if args.a < args.b { + return Err(ContractError::Custom(69)) + } + + Ok(()) +} diff --git a/src/example.rs b/src/example.rs @@ -0,0 +1,17 @@ +use anyhow::Result; +use borsh::BorshSerialize; +use pasta_curves::pallas; +use wasm_runtime::{runtime::Runtime, util::serialize_payload}; + +use smart_contract::Args; + +fn main() -> Result<()> { + let wasm_bytes = std::fs::read("smart_contract.wasm")?; + let mut runtime = Runtime::new(&wasm_bytes)?; + + let args = Args { a: pallas::Base::from(777), b: pallas::Base::from(666) }; + let payload = args.try_to_vec()?; + let input = serialize_payload(&payload); + + runtime.run(&input) +} diff --git a/src/lib.rs b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod memory; +pub mod runtime; +pub mod util; diff --git a/src/memory.rs b/src/memory.rs @@ -0,0 +1,41 @@ +use anyhow::Result; +use wasmer::{Array, Memory, WasmPtr}; + +use drk_sdk::error::ContractError; + +pub trait MemoryManipulation { + fn write(&self, mem_offset: u32, value_slice: &[u8]) -> Result<()>; + fn read(&self, mem_offset: u32, value_len: usize) -> Option<&[u8]>; +} + +impl MemoryManipulation for Memory { + fn write(&self, mem_offset: u32, value_slice: &[u8]) -> Result<()> { + let target_ptr: WasmPtr<u8, Array> = WasmPtr::new(mem_offset); + + let guest_value_slice = match target_ptr.deref(self, 0, value_slice.len() as u32) { + Some(slice) => slice, + None => [].to_vec(), + }; + + if guest_value_slice.is_empty() { + return Err(ContractError::OutOfMemory.into()) + } + + for i in 0..value_slice.len() { + guest_value_slice[i].set(value_slice[i]); + } + + Ok(()) + } + + fn read(&self, mem_offset: u32, value_len: usize) -> Option<&[u8]> { + let memory_size = self.size().bytes().0; + + if mem_offset as usize + value_len > memory_size || mem_offset as usize >= memory_size { + return None + } + + let ptr = unsafe { self.view::<u8>().as_ptr().add(mem_offset as usize) as *const u8 }; + unsafe { Some(std::slice::from_raw_parts(ptr, value_len)) } + } +} diff --git a/src/runtime.rs b/src/runtime.rs @@ -0,0 +1,88 @@ +use anyhow::Result; +use drk_sdk::error::ContractError; +use wasmer::{imports, Cranelift, Instance, Memory, Module, Store, Universal, Value}; + +use crate::memory::MemoryManipulation; + +const WASM_MEM_ALLOC: &str = "__drkruntime_mem_alloc"; +const MEMORY: &str = "memory"; +const ENTRYPOINT: &str = "entrypoint"; + +pub struct Runtime { + pub(crate) instance: Instance, +} + +impl Runtime { + pub fn new(wasm_bytes: &[u8]) -> Result<Self> { + let compiler = Cranelift::default(); + let store = Store::new(&Universal::new(compiler).engine()); + + println!("Compiling module..."); + let module = Module::new(&store, wasm_bytes)?; + + println!("Importing functions..."); + let import_object = imports! {}; + + println!("Instantiating module..."); + let instance = Instance::new(&module, &import_object)?; + + Ok(Self { instance }) + } + + pub fn run(&mut self, payload: &[u8]) -> Result<()> { + let memory = self.memory()?; + + let mem_offset = self.guest_mem_alloc(payload.len())?; + memory.write(mem_offset, payload)?; + + println!("Getting entrypoint function..."); + let entrypoint = self.instance.exports.get_function(ENTRYPOINT)?; + println!("{:#?}", entrypoint); + + println!("Executing wasm..."); + let mut contract_ret: u64 = 1; + match entrypoint.call(&[Value::I32(mem_offset as i32)]) { + Ok(v) => { + println!("wasm execution successful"); + match v[0] { + Value::I64(i) => { + println!("Contract returned: {}", i); + contract_ret = i as u64; + } + _ => unreachable!(), + } + } + + Err(e) => { + println!("wasm execution error: {:?}", e); + + let frames = e.trace(); + let frames_len = frames.len(); + + for i in 0..frames_len { + println!( + " Frame #{}: {:?}::{:?}", + frames_len - i, + frames[i].module_name(), + frames[i].function_name().or(Some("<func>")).unwrap() + ); + } + } + }; + + match ContractError::from(contract_ret) { + ContractError::Custom(0) => Ok(()), + e => Err(e.into()), + } + } + + fn guest_mem_alloc(&self, size: usize) -> Result<u32> { + let mem_alloc = self.instance.exports.get_function(WASM_MEM_ALLOC)?; + let res_target_ptr = mem_alloc.call(&[Value::I32(size as i32)])?.to_vec(); + Ok(res_target_ptr[0].unwrap_i32() as u32) + } + + fn memory(&self) -> Result<&Memory> { + Ok(self.instance.exports.get_memory(MEMORY)?) + } +} diff --git a/src/util.rs b/src/util.rs @@ -0,0 +1,10 @@ +/// Serialize payload to format accepted by the runtime entrypoint +pub fn serialize_payload(payload: &[u8]) -> Vec<u8> { + let mut out = vec![]; + + let len = payload.len() as u64; + out.extend_from_slice(&len.to_le_bytes()); + out.extend_from_slice(payload); + + out +}