Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add unit test for dynamic link trampoline #185

Merged
merged 5 commits into from
May 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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