use std::collections::HashSet;
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use crate::backend::{Backend, BackendApi, Querier, Storage};
use crate::capabilities::required_capabilities_from_module;
use crate::checksum::Checksum;
use crate::compatibility::check_wasm;
use crate::errors::{VmError, VmResult};
use crate::filesystem::mkdir_p;
use crate::instance::{Instance, InstanceOptions};
use crate::modules::{FileSystemCache, InMemoryCache, PinnedMemoryCache};
use crate::size::Size;
use crate::static_analysis::{deserialize_wasm, has_ibc_entry_points};
use crate::wasm_backend::{compile, make_runtime_store};
const STATE_DIR: &str = "state";
const WASM_DIR: &str = "wasm";
const CACHE_DIR: &str = "cache";
const MODULES_DIR: &str = "modules";
#[derive(Debug, Default, Clone, Copy)]
pub struct Stats {
pub hits_pinned_memory_cache: u32,
pub hits_memory_cache: u32,
pub hits_fs_cache: u32,
pub misses: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct Metrics {
pub stats: Stats,
pub elements_pinned_memory_cache: usize,
pub elements_memory_cache: usize,
pub size_pinned_memory_cache: usize,
pub size_memory_cache: usize,
}
#[derive(Clone, Debug)]
pub struct CacheOptions {
pub base_dir: PathBuf,
pub available_capabilities: HashSet<String>,
pub memory_cache_size: Size,
pub instance_memory_limit: Size,
}
pub struct CacheInner {
wasm_path: PathBuf,
instance_memory_limit: Size,
pinned_memory_cache: PinnedMemoryCache,
memory_cache: InMemoryCache,
fs_cache: FileSystemCache,
stats: Stats,
}
pub struct Cache<A: BackendApi, S: Storage, Q: Querier> {
available_capabilities: HashSet<String>,
inner: Mutex<CacheInner>,
type_api: PhantomData<A>,
type_storage: PhantomData<S>,
type_querier: PhantomData<Q>,
instantiation_lock: Mutex<()>,
}
#[derive(PartialEq, Eq, Debug)]
pub struct AnalysisReport {
pub has_ibc_entry_points: bool,
pub required_capabilities: HashSet<String>,
}
impl<A, S, Q> Cache<A, S, Q>
where
A: BackendApi + 'static, S: Storage + 'static, Q: Querier + 'static, {
pub unsafe fn new(options: CacheOptions) -> VmResult<Self> {
let CacheOptions {
base_dir,
available_capabilities,
memory_cache_size,
instance_memory_limit,
} = options;
let state_path = base_dir.join(STATE_DIR);
let cache_path = base_dir.join(CACHE_DIR);
let wasm_path = state_path.join(WASM_DIR);
mkdir_p(&state_path).map_err(|_e| VmError::cache_err("Error creating state directory"))?;
mkdir_p(&cache_path).map_err(|_e| VmError::cache_err("Error creating cache directory"))?;
mkdir_p(&wasm_path).map_err(|_e| VmError::cache_err("Error creating wasm directory"))?;
let fs_cache = FileSystemCache::new(cache_path.join(MODULES_DIR))
.map_err(|e| VmError::cache_err(format!("Error file system cache: {}", e)))?;
Ok(Cache {
available_capabilities,
inner: Mutex::new(CacheInner {
wasm_path,
instance_memory_limit,
pinned_memory_cache: PinnedMemoryCache::new(),
memory_cache: InMemoryCache::new(memory_cache_size),
fs_cache,
stats: Stats::default(),
}),
type_storage: PhantomData::<S>,
type_api: PhantomData::<A>,
type_querier: PhantomData::<Q>,
instantiation_lock: Mutex::new(()),
})
}
pub fn stats(&self) -> Stats {
self.inner.lock().unwrap().stats
}
pub fn metrics(&self) -> Metrics {
let cache = self.inner.lock().unwrap();
Metrics {
stats: cache.stats,
elements_pinned_memory_cache: cache.pinned_memory_cache.len(),
elements_memory_cache: cache.memory_cache.len(),
size_pinned_memory_cache: cache.pinned_memory_cache.size(),
size_memory_cache: cache.memory_cache.size(),
}
}
pub fn save_wasm(&self, wasm: &[u8]) -> VmResult<Checksum> {
check_wasm(wasm, &self.available_capabilities)?;
self.save_wasm_unchecked(wasm)
}
pub fn save_wasm_unchecked(&self, wasm: &[u8]) -> VmResult<Checksum> {
let module = compile(wasm, None, &[])?;
let mut cache = self.inner.lock().unwrap();
let checksum = save_wasm_to_disk(&cache.wasm_path, wasm)?;
cache.fs_cache.store(&checksum, &module)?;
Ok(checksum)
}
pub fn remove_wasm(&self, checksum: &Checksum) -> VmResult<()> {
let mut cache = self.inner.lock().unwrap();
cache.fs_cache.remove(checksum)?;
let path = &cache.wasm_path;
remove_wasm_from_disk(path, checksum)?;
Ok(())
}
pub fn load_wasm(&self, checksum: &Checksum) -> VmResult<Vec<u8>> {
self.load_wasm_with_path(&self.inner.lock().unwrap().wasm_path, checksum)
}
fn load_wasm_with_path(&self, wasm_path: &Path, checksum: &Checksum) -> VmResult<Vec<u8>> {
let code = load_wasm_from_disk(wasm_path, checksum)?;
if Checksum::generate(&code) != *checksum {
Err(VmError::integrity_err())
} else {
Ok(code)
}
}
pub fn analyze(&self, checksum: &Checksum) -> VmResult<AnalysisReport> {
let wasm = self.load_wasm(checksum)?;
let module = deserialize_wasm(&wasm)?;
Ok(AnalysisReport {
has_ibc_entry_points: has_ibc_entry_points(&module),
required_capabilities: required_capabilities_from_module(&module),
})
}
pub fn pin(&self, checksum: &Checksum) -> VmResult<()> {
let mut cache = self.inner.lock().unwrap();
if cache.pinned_memory_cache.has(checksum) {
return Ok(());
}
if let Some(module) = cache.memory_cache.load(checksum)? {
cache.stats.hits_memory_cache = cache.stats.hits_memory_cache.saturating_add(1);
return cache
.pinned_memory_cache
.store(checksum, module.module, module.size);
}
let store = make_runtime_store(Some(cache.instance_memory_limit));
if let Some(module) = cache.fs_cache.load(checksum, &store)? {
cache.stats.hits_fs_cache = cache.stats.hits_fs_cache.saturating_add(1);
let module_size = loupe::size_of_val(&module);
return cache
.pinned_memory_cache
.store(checksum, module, module_size);
}
let code = self.load_wasm_with_path(&cache.wasm_path, checksum)?;
cache.stats.misses = cache.stats.misses.saturating_add(1);
let module = compile(&code, Some(cache.instance_memory_limit), &[])?;
cache.fs_cache.store(checksum, &module)?;
let module_size = loupe::size_of_val(&module);
cache
.pinned_memory_cache
.store(checksum, module, module_size)
}
pub fn unpin(&self, checksum: &Checksum) -> VmResult<()> {
self.inner
.lock()
.unwrap()
.pinned_memory_cache
.remove(checksum)
}
pub fn get_instance(
&self,
checksum: &Checksum,
backend: Backend<A, S, Q>,
options: InstanceOptions,
) -> VmResult<Instance<A, S, Q>> {
let module = self.get_module(checksum)?;
let instance = Instance::from_module(
&module,
backend,
options.gas_limit,
options.print_debug,
None,
Some(&self.instantiation_lock),
)?;
Ok(instance)
}
fn get_module(&self, checksum: &Checksum) -> VmResult<wasmer::Module> {
let mut cache = self.inner.lock().unwrap();
if let Some(module) = cache.pinned_memory_cache.load(checksum)? {
cache.stats.hits_pinned_memory_cache =
cache.stats.hits_pinned_memory_cache.saturating_add(1);
return Ok(module);
}
if let Some(module) = cache.memory_cache.load(checksum)? {
cache.stats.hits_memory_cache = cache.stats.hits_memory_cache.saturating_add(1);
return Ok(module.module);
}
let store = make_runtime_store(Some(cache.instance_memory_limit));
if let Some(module) = cache.fs_cache.load(checksum, &store)? {
cache.stats.hits_fs_cache = cache.stats.hits_fs_cache.saturating_add(1);
let module_size = loupe::size_of_val(&module);
cache
.memory_cache
.store(checksum, module.clone(), module_size)?;
return Ok(module);
}
let wasm = self.load_wasm_with_path(&cache.wasm_path, checksum)?;
cache.stats.misses = cache.stats.misses.saturating_add(1);
let module = compile(&wasm, Some(cache.instance_memory_limit), &[])?;
cache.fs_cache.store(checksum, &module)?;
let module_size = loupe::size_of_val(&module);
cache
.memory_cache
.store(checksum, module.clone(), module_size)?;
Ok(module)
}
}
unsafe impl<A, S, Q> Sync for Cache<A, S, Q>
where
A: BackendApi + 'static,
S: Storage + 'static,
Q: Querier + 'static,
{
}
unsafe impl<A, S, Q> Send for Cache<A, S, Q>
where
A: BackendApi + 'static,
S: Storage + 'static,
Q: Querier + 'static,
{
}
fn save_wasm_to_disk(dir: impl Into<PathBuf>, wasm: &[u8]) -> VmResult<Checksum> {
let checksum = Checksum::generate(wasm);
let filename = checksum.to_hex();
let filepath = dir.into().join(filename).with_extension("wasm");
let mut file = OpenOptions::new()
.write(true)
.create(true)
.open(filepath)
.map_err(|e| VmError::cache_err(format!("Error opening Wasm file for writing: {}", e)))?;
file.write_all(wasm)
.map_err(|e| VmError::cache_err(format!("Error writing Wasm file: {}", e)))?;
Ok(checksum)
}
fn load_wasm_from_disk(dir: impl Into<PathBuf>, checksum: &Checksum) -> VmResult<Vec<u8>> {
let path = dir.into().join(checksum.to_hex());
let mut file = File::open(path.with_extension("wasm"))
.or_else(|_| File::open(path))
.map_err(|_e| VmError::cache_err("Error opening Wasm file for reading"))?;
let mut wasm = Vec::<u8>::new();
file.read_to_end(&mut wasm)
.map_err(|_e| VmError::cache_err("Error reading Wasm file"))?;
Ok(wasm)
}
fn remove_wasm_from_disk(dir: impl Into<PathBuf>, checksum: &Checksum) -> VmResult<()> {
let path = dir.into().join(checksum.to_hex());
let wasm_path = path.with_extension("wasm");
let path_exists = path.exists();
let wasm_path_exists = wasm_path.exists();
if !path_exists && !wasm_path_exists {
return Err(VmError::cache_err("Wasm file does not exist"));
}
if path_exists {
fs::remove_file(path)
.map_err(|_e| VmError::cache_err("Error removing Wasm file from disk"))?;
}
if wasm_path_exists {
fs::remove_file(wasm_path)
.map_err(|_e| VmError::cache_err("Error removing Wasm file from disk"))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calls::{call_execute, call_instantiate};
use crate::capabilities::capabilities_from_csv;
use crate::errors::VmError;
use crate::testing::{mock_backend, mock_env, mock_info, MockApi, MockQuerier, MockStorage};
use cosmwasm_std::{coins, Empty};
use std::fs::{create_dir_all, remove_dir_all, OpenOptions};
use std::io::Write;
use tempfile::TempDir;
const TESTING_GAS_LIMIT: u64 = 500_000_000_000; const TESTING_MEMORY_LIMIT: Size = Size::mebi(16);
const TESTING_OPTIONS: InstanceOptions = InstanceOptions {
gas_limit: TESTING_GAS_LIMIT,
print_debug: false,
};
const TESTING_MEMORY_CACHE_SIZE: Size = Size::mebi(200);
static CONTRACT: &[u8] = include_bytes!("../testdata/hackatom.wasm");
static IBC_CONTRACT: &[u8] = include_bytes!("../testdata/ibc_reflect.wasm");
static INVALID_CONTRACT_WAT: &str = r#"(module
(type $t0 (func (param i32) (result i32)))
(func $add_one (export "add_one") (type $t0) (param $p0 i32) (result i32)
get_local $p0
i32.const 1
i32.add))
"#;
fn default_capabilities() -> HashSet<String> {
capabilities_from_csv("iterator,staking")
}
fn make_testing_options() -> CacheOptions {
CacheOptions {
base_dir: TempDir::new().unwrap().into_path(),
available_capabilities: default_capabilities(),
memory_cache_size: TESTING_MEMORY_CACHE_SIZE,
instance_memory_limit: TESTING_MEMORY_LIMIT,
}
}
fn make_stargate_testing_options() -> CacheOptions {
let mut capabilities = default_capabilities();
capabilities.insert("stargate".into());
CacheOptions {
base_dir: TempDir::new().unwrap().into_path(),
available_capabilities: capabilities,
memory_cache_size: TESTING_MEMORY_CACHE_SIZE,
instance_memory_limit: TESTING_MEMORY_LIMIT,
}
}
#[test]
fn new_base_dir_will_be_created() {
let my_base_dir = TempDir::new()
.unwrap()
.into_path()
.join("non-existent-sub-dir");
let options = CacheOptions {
base_dir: my_base_dir.clone(),
..make_testing_options()
};
assert!(!my_base_dir.is_dir());
let _cache = unsafe { Cache::<MockApi, MockStorage, MockQuerier>::new(options).unwrap() };
assert!(my_base_dir.is_dir());
}
#[test]
fn save_wasm_works() {
let cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(make_testing_options()).unwrap() };
cache.save_wasm(CONTRACT).unwrap();
}
#[test]
fn save_wasm_allows_saving_multiple_times() {
let cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(make_testing_options()).unwrap() };
cache.save_wasm(CONTRACT).unwrap();
cache.save_wasm(CONTRACT).unwrap();
}
#[test]
fn save_wasm_rejects_invalid_contract() {
let wasm = wat::parse_str(INVALID_CONTRACT_WAT).unwrap();
let cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(make_testing_options()).unwrap() };
let save_result = cache.save_wasm(&wasm);
match save_result.unwrap_err() {
VmError::StaticValidationErr { msg, .. } => {
assert_eq!(msg, "Wasm contract doesn\'t have a memory section")
}
e => panic!("Unexpected error {:?}", e),
}
}
#[test]
fn save_wasm_fills_file_system_but_not_memory_cache() {
let cache = unsafe { Cache::new(make_testing_options()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
let backend = mock_backend(&[]);
let _ = cache
.get_instance(&checksum, backend, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 0);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
}
#[test]
fn save_wasm_unchecked_works() {
let cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(make_testing_options()).unwrap() };
cache.save_wasm_unchecked(CONTRACT).unwrap();
}
#[test]
fn save_wasm_unchecked_accepts_invalid_contract() {
let wasm = wat::parse_str(INVALID_CONTRACT_WAT).unwrap();
let cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(make_testing_options()).unwrap() };
cache.save_wasm_unchecked(&wasm).unwrap();
}
#[test]
fn load_wasm_works() {
let cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(make_testing_options()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
let restored = cache.load_wasm(&checksum).unwrap();
assert_eq!(restored, CONTRACT);
}
#[test]
fn load_wasm_works_across_multiple_cache_instances() {
let tmp_dir = TempDir::new().unwrap();
let id: Checksum;
{
let options1 = CacheOptions {
base_dir: tmp_dir.path().to_path_buf(),
available_capabilities: default_capabilities(),
memory_cache_size: TESTING_MEMORY_CACHE_SIZE,
instance_memory_limit: TESTING_MEMORY_LIMIT,
};
let cache1: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(options1).unwrap() };
id = cache1.save_wasm(CONTRACT).unwrap();
}
{
let options2 = CacheOptions {
base_dir: tmp_dir.path().to_path_buf(),
available_capabilities: default_capabilities(),
memory_cache_size: TESTING_MEMORY_CACHE_SIZE,
instance_memory_limit: TESTING_MEMORY_LIMIT,
};
let cache2: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(options2).unwrap() };
let restored = cache2.load_wasm(&id).unwrap();
assert_eq!(restored, CONTRACT);
}
}
#[test]
fn load_wasm_errors_for_non_existent_id() {
let cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(make_testing_options()).unwrap() };
let checksum = Checksum::from([
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5,
]);
match cache.load_wasm(&checksum).unwrap_err() {
VmError::CacheErr { msg, .. } => {
assert_eq!(msg, "Error opening Wasm file for reading")
}
e => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn load_wasm_errors_for_corrupted_wasm() {
let tmp_dir = TempDir::new().unwrap();
let options = CacheOptions {
base_dir: tmp_dir.path().to_path_buf(),
available_capabilities: default_capabilities(),
memory_cache_size: TESTING_MEMORY_CACHE_SIZE,
instance_memory_limit: TESTING_MEMORY_LIMIT,
};
let cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(options).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
let filepath = tmp_dir
.path()
.join(STATE_DIR)
.join(WASM_DIR)
.join(checksum.to_hex())
.with_extension("wasm");
let mut file = OpenOptions::new().write(true).open(filepath).unwrap();
file.write_all(b"broken data").unwrap();
let res = cache.load_wasm(&checksum);
match res {
Err(VmError::IntegrityErr { .. }) => {}
Err(e) => panic!("Unexpected error: {:?}", e),
Ok(_) => panic!("This must not succeed"),
}
}
#[test]
fn remove_wasm_works() {
let cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(make_testing_options()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
cache.load_wasm(&checksum).unwrap();
cache.remove_wasm(&checksum).unwrap();
match cache.load_wasm(&checksum).unwrap_err() {
VmError::CacheErr { msg, .. } => {
assert_eq!(msg, "Error opening Wasm file for reading")
}
e => panic!("Unexpected error: {:?}", e),
}
match cache.remove_wasm(&checksum).unwrap_err() {
VmError::CacheErr { msg, .. } => {
assert_eq!(msg, "Wasm file does not exist")
}
e => panic!("Unexpected error: {:?}", e),
}
}
#[test]
fn get_instance_finds_cached_module() {
let cache = unsafe { Cache::new(make_testing_options()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
let backend = mock_backend(&[]);
let _instance = cache
.get_instance(&checksum, backend, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 0);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
}
#[test]
fn get_instance_finds_cached_modules_and_stores_to_memory() {
let cache = unsafe { Cache::new(make_testing_options()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
let backend1 = mock_backend(&[]);
let backend2 = mock_backend(&[]);
let backend3 = mock_backend(&[]);
let backend4 = mock_backend(&[]);
let backend5 = mock_backend(&[]);
let _instance1 = cache
.get_instance(&checksum, backend1, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 0);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let _instance2 = cache
.get_instance(&checksum, backend2, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 1);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let _instance3 = cache
.get_instance(&checksum, backend3, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 2);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
cache.pin(&checksum).unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 3);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let _instance4 = cache
.get_instance(&checksum, backend4, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 1);
assert_eq!(cache.stats().hits_memory_cache, 3);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let _instance5 = cache
.get_instance(&checksum, backend5, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 2);
assert_eq!(cache.stats().hits_memory_cache, 3);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
}
#[test]
fn get_instance_recompiles_module() {
let options = make_testing_options();
let cache = unsafe { Cache::new(options.clone()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
remove_dir_all(options.base_dir.join(CACHE_DIR).join(MODULES_DIR)).unwrap();
let backend = mock_backend(&[]);
let _instance = cache
.get_instance(&checksum, backend, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 0);
assert_eq!(cache.stats().hits_fs_cache, 0);
assert_eq!(cache.stats().misses, 1);
let backend = mock_backend(&[]);
let _instance = cache
.get_instance(&checksum, backend, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 1);
assert_eq!(cache.stats().hits_fs_cache, 0);
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn call_instantiate_on_cached_contract() {
let cache = unsafe { Cache::new(make_testing_options()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
{
let mut instance = cache
.get_instance(&checksum, mock_backend(&[]), TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 0);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let info = mock_info("creator", &coins(1000, "earth"));
let msg = br#"{"verifier": "verifies", "beneficiary": "benefits"}"#;
let res =
call_instantiate::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg).unwrap();
let msgs = res.unwrap().messages;
assert_eq!(msgs.len(), 0);
}
{
let mut instance = cache
.get_instance(&checksum, mock_backend(&[]), TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 1);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let info = mock_info("creator", &coins(1000, "earth"));
let msg = br#"{"verifier": "verifies", "beneficiary": "benefits"}"#;
let res =
call_instantiate::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg).unwrap();
let msgs = res.unwrap().messages;
assert_eq!(msgs.len(), 0);
}
{
cache.pin(&checksum).unwrap();
let mut instance = cache
.get_instance(&checksum, mock_backend(&[]), TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 1);
assert_eq!(cache.stats().hits_memory_cache, 2);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let info = mock_info("creator", &coins(1000, "earth"));
let msg = br#"{"verifier": "verifies", "beneficiary": "benefits"}"#;
let res =
call_instantiate::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg).unwrap();
let msgs = res.unwrap().messages;
assert_eq!(msgs.len(), 0);
}
}
#[test]
fn call_execute_on_cached_contract() {
let cache = unsafe { Cache::new(make_testing_options()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
{
let mut instance = cache
.get_instance(&checksum, mock_backend(&[]), TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 0);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let info = mock_info("creator", &coins(1000, "earth"));
let msg = br#"{"verifier": "verifies", "beneficiary": "benefits"}"#;
let response =
call_instantiate::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg)
.unwrap()
.unwrap();
assert_eq!(response.messages.len(), 0);
let info = mock_info("verifies", &coins(15, "earth"));
let msg = br#"{"release":{}}"#;
let response = call_execute::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg)
.unwrap()
.unwrap();
assert_eq!(response.messages.len(), 1);
}
{
let mut instance = cache
.get_instance(&checksum, mock_backend(&[]), TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 1);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let info = mock_info("creator", &coins(1000, "earth"));
let msg = br#"{"verifier": "verifies", "beneficiary": "benefits"}"#;
let response =
call_instantiate::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg)
.unwrap()
.unwrap();
assert_eq!(response.messages.len(), 0);
let info = mock_info("verifies", &coins(15, "earth"));
let msg = br#"{"release":{}}"#;
let response = call_execute::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg)
.unwrap()
.unwrap();
assert_eq!(response.messages.len(), 1);
}
{
cache.pin(&checksum).unwrap();
let mut instance = cache
.get_instance(&checksum, mock_backend(&[]), TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 1);
assert_eq!(cache.stats().hits_memory_cache, 2);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let info = mock_info("creator", &coins(1000, "earth"));
let msg = br#"{"verifier": "verifies", "beneficiary": "benefits"}"#;
let response =
call_instantiate::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg)
.unwrap()
.unwrap();
assert_eq!(response.messages.len(), 0);
let info = mock_info("verifies", &coins(15, "earth"));
let msg = br#"{"release":{}}"#;
let response = call_execute::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg)
.unwrap()
.unwrap();
assert_eq!(response.messages.len(), 1);
}
}
#[test]
fn use_multiple_cached_instances_of_same_contract() {
let cache = unsafe { Cache::new(make_testing_options()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
let backend1 = mock_backend(&[]);
let backend2 = mock_backend(&[]);
let mut instance = cache
.get_instance(&checksum, backend1, TESTING_OPTIONS)
.unwrap();
let info = mock_info("owner1", &coins(1000, "earth"));
let msg = br#"{"verifier": "sue", "beneficiary": "mary"}"#;
let res =
call_instantiate::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg).unwrap();
let msgs = res.unwrap().messages;
assert_eq!(msgs.len(), 0);
let backend1 = instance.recycle().unwrap();
let mut instance = cache
.get_instance(&checksum, backend2, TESTING_OPTIONS)
.unwrap();
let info = mock_info("owner2", &coins(500, "earth"));
let msg = br#"{"verifier": "bob", "beneficiary": "john"}"#;
let res =
call_instantiate::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg).unwrap();
let msgs = res.unwrap().messages;
assert_eq!(msgs.len(), 0);
let backend2 = instance.recycle().unwrap();
let mut instance = cache
.get_instance(&checksum, backend2, TESTING_OPTIONS)
.unwrap();
let info = mock_info("bob", &coins(15, "earth"));
let msg = br#"{"release":{}}"#;
let res = call_execute::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg).unwrap();
let msgs = res.unwrap().messages;
assert_eq!(1, msgs.len());
let mut instance = cache
.get_instance(&checksum, backend1, TESTING_OPTIONS)
.unwrap();
let info = mock_info("sue", &coins(15, "earth"));
let msg = br#"{"release":{}}"#;
let res = call_execute::<_, _, _, Empty>(&mut instance, &mock_env(), &info, msg).unwrap();
let msgs = res.unwrap().messages;
assert_eq!(1, msgs.len());
}
#[test]
fn resets_gas_when_reusing_instance() {
let cache = unsafe { Cache::new(make_testing_options()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
let backend1 = mock_backend(&[]);
let backend2 = mock_backend(&[]);
let mut instance1 = cache
.get_instance(&checksum, backend1, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 0);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let original_gas = instance1.get_gas_left();
let info = mock_info("owner1", &coins(1000, "earth"));
let msg = br#"{"verifier": "sue", "beneficiary": "mary"}"#;
call_instantiate::<_, _, _, Empty>(&mut instance1, &mock_env(), &info, msg)
.unwrap()
.unwrap();
assert!(instance1.get_gas_left() < original_gas);
let instance2 = cache
.get_instance(&checksum, backend2, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 1);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
assert_eq!(instance2.get_gas_left(), TESTING_GAS_LIMIT);
}
#[test]
fn recovers_from_out_of_gas() {
let cache = unsafe { Cache::new(make_testing_options()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
let backend1 = mock_backend(&[]);
let backend2 = mock_backend(&[]);
let options = InstanceOptions {
gas_limit: 10,
print_debug: false,
};
let mut instance1 = cache.get_instance(&checksum, backend1, options).unwrap();
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let info1 = mock_info("owner1", &coins(1000, "earth"));
let msg1 = br#"{"verifier": "sue", "beneficiary": "mary"}"#;
match call_instantiate::<_, _, _, Empty>(&mut instance1, &mock_env(), &info1, msg1)
.unwrap_err()
{
VmError::GasDepletion { .. } => (), e => panic!("unexpected error, {:?}", e),
}
assert_eq!(instance1.get_gas_left(), 0);
let options = InstanceOptions {
gas_limit: TESTING_GAS_LIMIT,
print_debug: false,
};
let mut instance2 = cache.get_instance(&checksum, backend2, options).unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 1);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
assert_eq!(instance2.get_gas_left(), TESTING_GAS_LIMIT);
let info2 = mock_info("owner2", &coins(500, "earth"));
let msg2 = br#"{"verifier": "bob", "beneficiary": "john"}"#;
call_instantiate::<_, _, _, Empty>(&mut instance2, &mock_env(), &info2, msg2)
.unwrap()
.unwrap();
}
#[test]
fn save_wasm_to_disk_works_for_same_data_multiple_times() {
let tmp_dir = TempDir::new().unwrap();
let path = tmp_dir.path();
let code = vec![12u8; 17];
save_wasm_to_disk(path, &code).unwrap();
save_wasm_to_disk(path, &code).unwrap();
}
#[test]
fn save_wasm_to_disk_fails_on_non_existent_dir() {
let tmp_dir = TempDir::new().unwrap();
let path = tmp_dir.path().join("something");
let code = vec![12u8; 17];
let res = save_wasm_to_disk(path.to_str().unwrap(), &code);
assert!(res.is_err());
}
#[test]
fn load_wasm_from_disk_works() {
let tmp_dir = TempDir::new().unwrap();
let path = tmp_dir.path();
let code = vec![12u8; 17];
let checksum = save_wasm_to_disk(path, &code).unwrap();
let loaded = load_wasm_from_disk(path, &checksum).unwrap();
assert_eq!(code, loaded);
}
#[test]
fn load_wasm_from_disk_works_in_subfolder() {
let tmp_dir = TempDir::new().unwrap();
let path = tmp_dir.path().join("something");
create_dir_all(&path).unwrap();
let code = vec![12u8; 17];
let checksum = save_wasm_to_disk(&path, &code).unwrap();
let loaded = load_wasm_from_disk(&path, &checksum).unwrap();
assert_eq!(code, loaded);
}
#[test]
fn remove_wasm_from_disk_works() {
let tmp_dir = TempDir::new().unwrap();
let path = tmp_dir.path();
let code = vec![12u8; 17];
let checksum = save_wasm_to_disk(path, &code).unwrap();
remove_wasm_from_disk(path, &checksum).unwrap();
match remove_wasm_from_disk(path, &checksum).unwrap_err() {
VmError::CacheErr { msg } => assert_eq!(msg, "Wasm file does not exist"),
err => panic!("Unexpected error: {:?}", err),
}
}
#[test]
fn analyze_works() {
let cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(make_stargate_testing_options()).unwrap() };
let checksum1 = cache.save_wasm(CONTRACT).unwrap();
let report1 = cache.analyze(&checksum1).unwrap();
assert_eq!(
report1,
AnalysisReport {
has_ibc_entry_points: false,
required_capabilities: HashSet::new(),
}
);
let checksum2 = cache.save_wasm(IBC_CONTRACT).unwrap();
let report2 = cache.analyze(&checksum2).unwrap();
assert_eq!(
report2,
AnalysisReport {
has_ibc_entry_points: true,
required_capabilities: HashSet::from_iter([
"iterator".to_string(),
"stargate".to_string()
]),
}
);
}
#[test]
fn pin_unpin_works() {
let cache = unsafe { Cache::new(make_testing_options()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
let backend = mock_backend(&[]);
let _instance = cache
.get_instance(&checksum, backend, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 0);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
cache.pin(&checksum).unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 1);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
cache.pin(&checksum).unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 1);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
let backend = mock_backend(&[]);
let _instance = cache
.get_instance(&checksum, backend, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 1);
assert_eq!(cache.stats().hits_memory_cache, 1);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
cache.unpin(&checksum).unwrap();
let backend = mock_backend(&[]);
let _instance = cache
.get_instance(&checksum, backend, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 1);
assert_eq!(cache.stats().hits_memory_cache, 2);
assert_eq!(cache.stats().hits_fs_cache, 1);
assert_eq!(cache.stats().misses, 0);
cache.unpin(&checksum).unwrap();
let non_id = Checksum::generate(b"non_existent");
cache.unpin(&non_id).unwrap();
}
#[test]
fn pin_recompiles_module() {
let options = make_testing_options();
let cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(options.clone()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
remove_dir_all(options.base_dir.join(CACHE_DIR).join(MODULES_DIR)).unwrap();
cache.pin(&checksum).unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 0);
assert_eq!(cache.stats().hits_memory_cache, 0);
assert_eq!(cache.stats().hits_fs_cache, 0);
assert_eq!(cache.stats().misses, 1);
let backend = mock_backend(&[]);
let _ = cache
.get_instance(&checksum, backend, TESTING_OPTIONS)
.unwrap();
assert_eq!(cache.stats().hits_pinned_memory_cache, 1);
assert_eq!(cache.stats().hits_memory_cache, 0);
assert_eq!(cache.stats().hits_fs_cache, 0);
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn loading_without_extension_works() {
let tmp_dir = TempDir::new().unwrap();
let options = CacheOptions {
base_dir: tmp_dir.path().to_path_buf(),
available_capabilities: default_capabilities(),
memory_cache_size: TESTING_MEMORY_CACHE_SIZE,
instance_memory_limit: TESTING_MEMORY_LIMIT,
};
let cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(options).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();
let old_path = tmp_dir
.path()
.join(STATE_DIR)
.join(WASM_DIR)
.join(checksum.to_hex());
let new_path = old_path.with_extension("wasm");
fs::rename(new_path, old_path).unwrap();
let restored = cache.load_wasm(&checksum).unwrap();
assert_eq!(restored, CONTRACT);
}
}