Skip to content

Commit

Permalink
test: add unit test for dynamic link trampoline (#185)
Browse files Browse the repository at this point in the history
* refactor: remove available gas parameter by contract_call

* test: add unit test for native_dynamic_link_trampoline

* refactor: refactor test code for using an existing mock_instance function

* chore: cargo fmt

* chore: rename function
  • Loading branch information
Jiyong Ha authored May 9, 2022
1 parent df82d81 commit 141c988
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 73 deletions.
1 change: 0 additions & 1 deletion packages/vm/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ pub trait BackendApi: Copy + Clone + Send {
contract_addr: &str,
target_info: &FunctionMetadata,
args: &[WasmerVal],
gas: u64,
) -> BackendResult<Box<[WasmerVal]>>
where
A: BackendApi + 'static,
Expand Down
233 changes: 169 additions & 64 deletions packages/vm/src/dynamic_link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,7 @@ where
Err(_) => return Err(RuntimeError::new("Invalid stored callee contract address")),
};

let (call_result, gas_info) =
env.api
.contract_call(env, contract_addr, &func_info, args, env.get_gas_left());
let (call_result, gas_info) = env.api.contract_call(env, contract_addr, &func_info, args);
process_gas_info::<A, S, Q>(env, gas_info)?;
match call_result {
Ok(ret) => Ok(ret.to_vec()),
Expand Down Expand Up @@ -193,16 +191,14 @@ where
#[cfg(test)]
mod tests {
use super::*;
use std::ptr::NonNull;
use wasmer::{imports, Function, Instance as WasmerInstance};
use cosmwasm_std::{coins, Empty};
use std::cell::RefCell;

use crate::size::Size;
use crate::testing::{
mock_env, read_data_from_mock_env, write_data_to_mock_env, MockApi, MockQuerier,
MockStorage,
mock_env, mock_instance, read_data_from_mock_env, write_data_to_mock_env, MockApi,
MockQuerier, MockStorage, INSTANCE_CACHE,
};
use crate::to_vec;
use crate::wasm_backend::compile;
use crate::VmError;

static CONTRACT: &[u8] = include_bytes!("../testdata/hackatom.wasm");
Expand All @@ -211,70 +207,57 @@ mod tests {
const PADDING_DATA: &[u8] = b"deadbeef";
const PASS_DATA1: &[u8] = b"data";

const TESTING_GAS_LIMIT: u64 = 500_000;
const TESTING_MEMORY_LIMIT: Option<Size> = Some(Size::mebi(16));
const CALLEE_NAME_ADDR: &str = "callee";
const CALLER_NAME_ADDR: &str = "caller";

fn make_instance(
api: MockApi,
) -> (
Environment<MockApi, MockStorage, MockQuerier>,
Box<WasmerInstance>,
) {
let gas_limit = TESTING_GAS_LIMIT;
let env = Environment::new(api, gas_limit, false);

let module = compile(&CONTRACT, TESTING_MEMORY_LIMIT).unwrap();
let store = module.store();
// we need stubs for all required imports
let import_obj = imports! {
"env" => {
"db_read" => Function::new_native(&store, |_a: u32| -> u32 { 0 }),
"db_write" => Function::new_native(&store, |_a: u32, _b: u32| {}),
"db_remove" => Function::new_native(&store, |_a: u32| {}),
"db_scan" => Function::new_native(&store, |_a: u32, _b: u32, _c: i32| -> u32 { 0 }),
"db_next" => Function::new_native(&store, |_a: u32| -> u32 { 0 }),
"query_chain" => Function::new_native(&store, |_a: u32| -> u32 { 0 }),
"addr_validate" => Function::new_native(&store, |_a: u32| -> u32 { 0 }),
"addr_canonicalize" => Function::new_native(&store, |_a: u32, _b: u32| -> u32 { 0 }),
"addr_humanize" => Function::new_native(&store, |_a: u32, _b: u32| -> u32 { 0 }),
"secp256k1_verify" => Function::new_native(&store, |_a: u32, _b: u32, _c: u32| -> u32 { 0 }),
"secp256k1_recover_pubkey" => Function::new_native(&store, |_a: u32, _b: u32, _c: u32| -> u64 { 0 }),
"ed25519_verify" => Function::new_native(&store, |_a: u32, _b: u32, _c: u32| -> u32 { 0 }),
"ed25519_batch_verify" => Function::new_native(&store, |_a: u32, _b: u32, _c: u32| -> u32 { 0 }),
"sha1_calculate" => Function::new_native(&store, |_a: u32| -> u64 { 0 }),
"debug" => Function::new_native(&store, |_a: u32| {}),
},
};
let instance = Box::from(WasmerInstance::new(&module, &import_obj).unwrap());
// this account has some coins
const INIT_ADDR: &str = "someone";
const INIT_AMOUNT: u128 = 500;
const INIT_DENOM: &str = "TOKEN";

let instance_ptr = NonNull::from(instance.as_ref());
env.set_wasmer_instance(Some(instance_ptr));
env.set_gas_left(gas_limit);
fn prepare_dynamic_call_data(
callee_address: Option<String>,
func_info: FunctionMetadata,
caller_env: &mut Environment<MockApi, MockStorage, MockQuerier>,
) {
let target_module_name = func_info.module_name.clone();
caller_env.set_callee_function_metadata(Some(func_info));

let serialized_env = to_vec(&mock_env()).unwrap();
env.set_serialized_env(&serialized_env);

(env, instance)
caller_env.set_serialized_env(&serialized_env);

let mut storage = MockStorage::new();
match callee_address {
Some(addr) => {
storage
.set(target_module_name.as_bytes(), addr.as_bytes())
.0
.expect("error setting value");
}
_ => {}
}
let querier: MockQuerier<Empty> =
MockQuerier::new(&[(INIT_ADDR, &coins(INIT_AMOUNT, INIT_DENOM))]);
caller_env.move_in(storage, querier);
}

#[test]
fn copy_single_region_works() {
let api = MockApi::default();
let (src_env, _src_instance) = make_instance(api);
let (dst_env, _dst_instance) = make_instance(api);
let src_instance = mock_instance(&CONTRACT, &[]);
let dst_instance = mock_instance(&CONTRACT, &[]);

let data_wasm_ptr = write_data_to_mock_env(&src_env, PASS_DATA1).unwrap();
let data_wasm_ptr = write_data_to_mock_env(&src_instance.env, PASS_DATA1).unwrap();
let copy_result = copy_region_vals_between_env(
&src_env,
&dst_env,
&src_instance.env,
&dst_instance.env,
&[WasmerVal::I32(data_wasm_ptr as i32)],
true,
)
.unwrap();
assert_eq!(copy_result.len(), 1);

let read_result =
read_data_from_mock_env(&dst_env, &copy_result[0], PASS_DATA1.len()).unwrap();
read_data_from_mock_env(&dst_instance.env, &copy_result[0], PASS_DATA1.len()).unwrap();
assert_eq!(PASS_DATA1, read_result);

// Even after deallocate, wasm region data remains.
Expand All @@ -288,28 +271,150 @@ mod tests {

#[test]
fn wrong_use_copied_region_fails() {
let api = MockApi::default();
let (src_env, _src_instance) = make_instance(api);
let (dst_env, _dst_instance) = make_instance(api);
let src_instance = mock_instance(&CONTRACT, &[]);
let dst_instance = mock_instance(&CONTRACT, &[]);

// If there is no padding data, it is difficult to compare because the same memory index falls apart.
write_data_to_mock_env(&src_env, PADDING_DATA).unwrap();
write_data_to_mock_env(&src_instance.env, PADDING_DATA).unwrap();

let data_wasm_ptr = write_data_to_mock_env(&src_env, PASS_DATA1).unwrap();
let data_wasm_ptr = write_data_to_mock_env(&src_instance.env, PASS_DATA1).unwrap();
let copy_result = copy_region_vals_between_env(
&src_env,
&dst_env,
&src_instance.env,
&dst_instance.env,
&[WasmerVal::I32(data_wasm_ptr as i32)],
true,
)
.unwrap();
assert_eq!(copy_result.len(), 1);

let read_from_src_result =
read_data_from_mock_env(&src_env, &copy_result[0], PASS_DATA1.len());
read_data_from_mock_env(&src_instance.env, &copy_result[0], PASS_DATA1.len());
assert!(matches!(
read_from_src_result,
Err(VmError::CommunicationErr { .. })
));
}

fn init_cache_with_two_instances() {
let callee_wasm = wat::parse_str(
r#"(module
(memory 3)
(export "memory" (memory 0))
(export "interface_version_5" (func 0))
(export "instantiate" (func 0))
(export "allocate" (func 0))
(export "deallocate" (func 0))
(type (func))
(func (type 0) nop)
(export "foo" (func 0))
)"#,
)
.unwrap();
let caller_wasm = wat::parse_str(
r#"(module
(memory 3)
(export "memory" (memory 0))
(export "interface_version_5" (func 0))
(export "instantiate" (func 0))
(export "allocate" (func 0))
(export "deallocate" (func 0))
(type (func))
(func (type 0) nop)
)"#,
)
.unwrap();

INSTANCE_CACHE.with(|lock| {
let mut cache = lock.write().unwrap();
cache.insert(
CALLEE_NAME_ADDR.to_string(),
RefCell::new(mock_instance(&callee_wasm, &[])),
);
cache.insert(
CALLER_NAME_ADDR.to_string(),
RefCell::new(mock_instance(&caller_wasm, &[])),
);
});
}

#[test]
fn native_dynamic_link_trampoline_works() {
init_cache_with_two_instances();

INSTANCE_CACHE.with(|lock| {
let cache = lock.read().unwrap();
let caller_instance = cache.get(CALLER_NAME_ADDR).unwrap();
let mut caller_env = &mut caller_instance.borrow_mut().env;
let target_func_info = FunctionMetadata {
module_name: CALLER_NAME_ADDR.to_string(),
name: "foo".to_string(),
signature: ([], []).into(),
};
prepare_dynamic_call_data(
Some(CALLEE_NAME_ADDR.to_string()),
target_func_info,
&mut caller_env,
);

let result = native_dynamic_link_trampoline(&caller_env, &[]).unwrap();
assert_eq!(result.len(), 0);
});
}

#[test]
fn native_dynamic_link_trampoline_do_not_specify_callee_address_fail() {
init_cache_with_two_instances();

INSTANCE_CACHE.with(|lock| {
let cache = lock.read().unwrap();
let caller_instance = cache.get(CALLER_NAME_ADDR).unwrap();
let mut caller_env = &mut caller_instance.borrow_mut().env;
let target_func_info = FunctionMetadata {
module_name: CALLER_NAME_ADDR.to_string(),
name: "foo".to_string(),
signature: ([], []).into(),
};
prepare_dynamic_call_data(None, target_func_info, &mut caller_env);

let result = native_dynamic_link_trampoline(&caller_env, &[]);
assert!(matches!(result, Err(RuntimeError { .. })));

assert_eq!(
result.err().unwrap().message(),
"cannot found the callee contract address in the storage"
);
});
}

#[test]
fn native_dynamic_link_trampoline_not_exist_callee_address_fails() {
init_cache_with_two_instances();

INSTANCE_CACHE.with(|lock| {
let cache = lock.read().unwrap();
let caller_instance = cache.get(CALLER_NAME_ADDR).unwrap();
let mut caller_env = &mut caller_instance.borrow_mut().env;
let target_func_info = FunctionMetadata {
module_name: CALLER_NAME_ADDR.to_string(),
name: "foo".to_string(),
signature: ([], []).into(),
};
prepare_dynamic_call_data(
Some("invalid_address".to_string()),
target_func_info,
&mut caller_env,
);

let result = native_dynamic_link_trampoline(&caller_env, &[]);
assert!(matches!(
result,
Err(RuntimeError { .. })
));

assert_eq!(result.err().unwrap().message(),
"func_info:{module_name:caller, name:foo, signature:[] -> []}, error:Unknown error during call into backend: Some(\"cannot found contract\")"
);
});
}
}
60 changes: 53 additions & 7 deletions packages/vm/src/testing/mock.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
use cosmwasm_std::testing::{digit_sum, riffle_shuffle};
use cosmwasm_std::{Addr, BlockInfo, Coin, ContractInfo, Env, MessageInfo, Timestamp};
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::RwLock;
use std::thread_local;

use super::querier::MockQuerier;
use super::storage::MockStorage;
use crate::environment::Environment;
use crate::{Backend, BackendApi, BackendError, BackendResult, GasInfo, Querier, Storage};
use crate::instance::Instance;
use crate::{
copy_region_vals_between_env, Backend, BackendApi, BackendError, BackendResult, GasInfo,
Querier, Storage,
};
use crate::{FunctionMetadata, WasmerVal};

pub const MOCK_CONTRACT_ADDR: &str = "cosmos2contract";
Expand Down Expand Up @@ -33,6 +41,13 @@ pub fn mock_backend_with_balances(
}
}

type MockInstance = Instance<MockApi, MockStorage, MockQuerier>;
thread_local! {
// INSTANCE_CACHE is intended to replace wasmvm's cache layer in the mock.
// Unlike wasmvm, you have to initialize it yourself in the place where you test the dynamic call.
pub static INSTANCE_CACHE: RwLock<HashMap<String, RefCell<MockInstance>>> = RwLock::new(HashMap::new());
}

/// Zero-pads all human addresses to make them fit the canonical_length and
/// trims off zeros for the reverse operation.
/// This is not really smart, but allows us to see a difference (and consistent length for canonical adddresses).
Expand Down Expand Up @@ -152,18 +167,49 @@ impl BackendApi for MockApi {
}
fn contract_call<A, S, Q>(
&self,
_: &Environment<A, S, Q>,
_: &str,
_: &FunctionMetadata,
_: &[WasmerVal],
_: u64,
caller_env: &Environment<A, S, Q>,
contract_addr: &str,
func_info: &FunctionMetadata,
args: &[WasmerVal],
) -> BackendResult<Box<[WasmerVal]>>
where
A: BackendApi + 'static,
S: Storage + 'static,
Q: Querier + 'static,
{
panic!("get_contract_call for the mock will be filled later")
let mut gas_info = GasInfo::new(0, 0);
INSTANCE_CACHE.with(|lock| {
let cache = lock.read().unwrap();
match cache.get(contract_addr) {
Some(callee_instance_cell) => {
let callee_instance = callee_instance_cell.borrow_mut();

let arg_region_ptrs =
copy_region_vals_between_env(caller_env, &callee_instance.env, args, false)
.unwrap();
let call_ret = match callee_instance.call_function_strict(
&func_info.signature,
&func_info.name,
&arg_region_ptrs,
) {
Ok(rets) => Ok(copy_region_vals_between_env(
&callee_instance.env,
caller_env,
&rets,
true,
)
.unwrap()),
Err(e) => Err(BackendError::unknown(e.to_string())),
};
gas_info.cost += callee_instance.create_gas_report().used_internally;
(call_ret, gas_info)
}
None => (
Err(BackendError::unknown("cannot found contract")),
gas_info,
),
}
})
}
}

Expand Down
Loading

0 comments on commit 141c988

Please sign in to comment.