From 141c988a1e46066d4d480f938b171da8235e1224 Mon Sep 17 00:00:00 2001 From: Jiyong Ha Date: Mon, 9 May 2022 14:07:42 +0900 Subject: [PATCH] test: add unit test for dynamic link trampoline (#185) * 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 --- packages/vm/src/backend.rs | 1 - packages/vm/src/dynamic_link.rs | 233 +++++++++++++++++++++++--------- packages/vm/src/testing/mock.rs | 60 +++++++- packages/vm/src/testing/mod.rs | 3 +- 4 files changed, 224 insertions(+), 73 deletions(-) diff --git a/packages/vm/src/backend.rs b/packages/vm/src/backend.rs index d95faaad1..ae38e24bd 100644 --- a/packages/vm/src/backend.rs +++ b/packages/vm/src/backend.rs @@ -136,7 +136,6 @@ pub trait BackendApi: Copy + Clone + Send { contract_addr: &str, target_info: &FunctionMetadata, args: &[WasmerVal], - gas: u64, ) -> BackendResult> where A: BackendApi + 'static, diff --git a/packages/vm/src/dynamic_link.rs b/packages/vm/src/dynamic_link.rs index 035bf40a3..ddb400043 100644 --- a/packages/vm/src/dynamic_link.rs +++ b/packages/vm/src/dynamic_link.rs @@ -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::(env, gas_info)?; match call_result { Ok(ret) => Ok(ret.to_vec()), @@ -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"); @@ -211,62 +207,49 @@ 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 = Some(Size::mebi(16)); + const CALLEE_NAME_ADDR: &str = "callee"; + const CALLER_NAME_ADDR: &str = "caller"; - fn make_instance( - api: MockApi, - ) -> ( - Environment, - Box, - ) { - 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, + func_info: FunctionMetadata, + caller_env: &mut Environment, + ) { + 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 = + 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, ) @@ -274,7 +257,7 @@ mod tests { assert_eq!(copy_result.len(), 1); let read_result = - read_data_from_mock_env(&dst_env, ©_result[0], PASS_DATA1.len()).unwrap(); + read_data_from_mock_env(&dst_instance.env, ©_result[0], PASS_DATA1.len()).unwrap(); assert_eq!(PASS_DATA1, read_result); // Even after deallocate, wasm region data remains. @@ -288,17 +271,16 @@ 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, ) @@ -306,10 +288,133 @@ mod tests { assert_eq!(copy_result.len(), 1); let read_from_src_result = - read_data_from_mock_env(&src_env, ©_result[0], PASS_DATA1.len()); + read_data_from_mock_env(&src_instance.env, ©_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\")" + ); + }); + } } diff --git a/packages/vm/src/testing/mock.rs b/packages/vm/src/testing/mock.rs index c2fe140e0..19d87e45f 100644 --- a/packages/vm/src/testing/mock.rs +++ b/packages/vm/src/testing/mock.rs @@ -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"; @@ -33,6 +41,13 @@ pub fn mock_backend_with_balances( } } +type MockInstance = Instance; +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>> = 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). @@ -152,18 +167,49 @@ impl BackendApi for MockApi { } fn contract_call( &self, - _: &Environment, - _: &str, - _: &FunctionMetadata, - _: &[WasmerVal], - _: u64, + caller_env: &Environment, + contract_addr: &str, + func_info: &FunctionMetadata, + args: &[WasmerVal], ) -> BackendResult> 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, + ), + } + }) } } diff --git a/packages/vm/src/testing/mod.rs b/packages/vm/src/testing/mod.rs index e4ca78880..4d436efbe 100644 --- a/packages/vm/src/testing/mod.rs +++ b/packages/vm/src/testing/mod.rs @@ -24,7 +24,8 @@ pub use instance::{ test_io, MockInstanceOptions, }; pub use mock::{ - mock_backend, mock_backend_with_balances, mock_env, mock_info, MockApi, MOCK_CONTRACT_ADDR, + mock_backend, mock_backend_with_balances, mock_env, mock_info, MockApi, INSTANCE_CACHE, + MOCK_CONTRACT_ADDR, }; pub use querier::MockQuerier; pub use result::{TestingError, TestingResult};