Skip to content

Commit

Permalink
Merge pull request #736 from CosmWasm/cache-analyze
Browse files Browse the repository at this point in the history
Add Cache::analyze
  • Loading branch information
ethanfrey authored Jan 20, 2021
2 parents 9c1fd5c + bd0ee68 commit 31a5d0d
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 215 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ workflows:
- update-wasmer
- metering-restart
- load-wasm-speed
- cache-analyze
deploy:
jobs:
- build_and_upload_devcontracts:
Expand Down
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ and this project adheres to
### Added

- cosmwasm-vm: Add PinnedMemoryCache. ([#696])
- cosmwasm-vm: The new `Instance::has_ibc_entry_points` tells the caller if the
contract exposes IBC entry points.
- cosmwasm-vm: The new `Cache::analyze` provides a static analyzis of the Wasm
bytecode. This is used to tell the caller if the contract exposes IBC entry
points. ([#736])

### Changed

Expand All @@ -31,6 +32,7 @@ and this project adheres to

[#696]: https://github.com/CosmWasm/cosmwasm/issues/696
[#697]: https://github.com/CosmWasm/cosmwasm/issues/697
[#736]: https://github.com/CosmWasm/cosmwasm/pull/736

## [0.13.2] - 2021-01-14

Expand Down
6 changes: 0 additions & 6 deletions contracts/ibc-reflect/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,6 @@ fn connect<T: Into<HumanAddr>>(
let _: HandleResponse = handle(deps, mock_env(), info, handle_msg).unwrap();
}

#[test]
fn ibc_entry_points_are_detected() {
let deps = mock_instance(WASM, &[]);
assert_eq!(deps.has_ibc_entry_points(), true);
}

#[test]
fn init_works() {
let mut deps = mock_instance(WASM, &[]);
Expand Down
6 changes: 3 additions & 3 deletions packages/vm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ cargo bench --no-default-features --features cranelift

`module_size` and `module_size.sh`

Memory profiling of compiled modules. `module_size.sh` executes `module_size`, and
uses valgrind's memory profiling tool (massif) to compute the amount of heap memory
used by a compiled module.
Memory profiling of compiled modules. `module_size.sh` executes `module_size`,
and uses valgrind's memory profiling tool (massif) to compute the amount of heap
memory used by a compiled module.

```
cd packages/vm
Expand Down
11 changes: 11 additions & 0 deletions packages/vm/benches/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ fn bench_cache(c: &mut Criterion) {
});
});

group.bench_function("analyze", |b| {
let mut cache: Cache<MockApi, MockStorage, MockQuerier> =
unsafe { Cache::new(options.clone()).unwrap() };
let checksum = cache.save_wasm(CONTRACT).unwrap();

b.iter(|| {
let result = cache.analyze(&checksum);
assert!(result.is_ok());
});
});

group.bench_function("instantiate from fs", |b| {
let non_memcache = CacheOptions {
base_dir: TempDir::new().unwrap().into_path(),
Expand Down
18 changes: 18 additions & 0 deletions packages/vm/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::errors::{VmError, VmResult};
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_and_use, compile_only, make_runtime_store};

const WASM_DIR: &str = "wasm";
Expand Down Expand Up @@ -50,6 +51,10 @@ pub struct Cache<A: Api, S: Storage, Q: Querier> {
type_querier: PhantomData<Q>,
}

pub struct AnalysisReport {
pub has_ibc_entry_points: bool,
}

impl<A, S, Q> Cache<A, S, Q>
where
A: Api + 'static, // 'static is needed by `impl<…> Instance`
Expand Down Expand Up @@ -119,6 +124,19 @@ where
}
}

/// Performs static anlyzation on this Wasm without compiling or instantiating it.
///
/// Once the contract was stored via [`save_wasm`], this can be called at any point in time.
/// It does not depend on any caching of the contract.
pub fn analyze(&self, checksum: &Checksum) -> VmResult<AnalysisReport> {
// Here we could use a streaming deserializer to slightly improve performance. However, this way it is DRYer.
let wasm = self.load_wasm(checksum)?;
let module = deserialize_wasm(&wasm)?;
Ok(AnalysisReport {
has_ibc_entry_points: has_ibc_entry_points(&module),
})
}

/// Pins a Module that was previously stored via save_wasm.
///
/// The module is lookup first in the memory cache, and then in the file system cache.
Expand Down
102 changes: 20 additions & 82 deletions packages/vm/src/compatibility.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use parity_wasm::elements::{deserialize_buffer, External, ImportEntry, Module};
use parity_wasm::elements::{External, ImportEntry, Module};
use std::collections::BTreeSet;
use std::collections::HashSet;

use crate::errors::{VmError, VmResult};
use crate::features::required_features_from_module;
use crate::limited::LimitedDisplay;
use crate::static_analysis::{deserialize_wasm, exported_functions};

/// Lists all imports we provide upon instantiating the instance in Instance::from_module()
/// This should be updated when new imports are added
Expand Down Expand Up @@ -35,18 +36,9 @@ const REQUIRED_EXPORTS: &[&str] = &[

const MEMORY_LIMIT: u32 = 512; // in pages

fn deserialize(wasm_code: &[u8]) -> VmResult<Module> {
deserialize_buffer(&wasm_code).map_err(|err| {
VmError::static_validation_err(format!(
"Wasm bytecode could not be deserialized. Deserialization error: \"{}\"",
err
))
})
}

/// Checks if the data is valid wasm and compatibility with the CosmWasm API (imports and exports)
pub fn check_wasm(wasm_code: &[u8], supported_features: &HashSet<String>) -> VmResult<()> {
let module = deserialize(wasm_code)?;
let module = deserialize_wasm(wasm_code)?;
check_wasm_memories(&module)?;
check_wasm_exports(&module)?;
check_wasm_imports(&module, SUPPORTED_IMPORTS)?;
Expand Down Expand Up @@ -91,16 +83,9 @@ fn check_wasm_memories(module: &Module) -> VmResult<()> {
}

fn check_wasm_exports(module: &Module) -> VmResult<()> {
let available_exports: Vec<String> = module.export_section().map_or(vec![], |export_section| {
export_section
.entries()
.iter()
.map(|entry| entry.field().to_string())
.collect()
});

let available_exports: HashSet<String> = exported_functions(module);
for required_export in REQUIRED_EXPORTS {
if !available_exports.iter().any(|x| x == required_export) {
if !available_exports.contains(*required_export) {
return Err(VmError::static_validation_err(format!(
"Wasm contract doesn't have required export: \"{}\". Exports required by VM: {:?}. Contract version too old for this VM?",
required_export, REQUIRED_EXPORTS
Expand Down Expand Up @@ -161,63 +146,16 @@ fn check_wasm_features(module: &Module, supported_features: &HashSet<String>) ->
mod tests {
use super::*;
use crate::errors::VmError;
use parity_wasm::elements::Internal;
use std::iter::FromIterator;

static CONTRACT_0_6: &[u8] = include_bytes!("../testdata/hackatom_0.6.wasm");
static CONTRACT_0_7: &[u8] = include_bytes!("../testdata/hackatom_0.7.wasm");
static CONTRACT: &[u8] = include_bytes!("../testdata/hackatom.wasm");
static CORRUPTED: &[u8] = include_bytes!("../testdata/corrupted.wasm");

fn default_features() -> HashSet<String> {
HashSet::from_iter(["staking".to_string()].iter().cloned())
}

#[test]
fn deserialize_works() {
let module = deserialize(CONTRACT).unwrap();
assert_eq!(module.version(), 1);

let exported_functions =
module
.export_section()
.unwrap()
.entries()
.iter()
.filter(|entry| {
if let Internal::Function(_) = entry.internal() {
true
} else {
false
}
});
assert_eq!(exported_functions.count(), 7); // 6 required export plus "migrate"

let exported_memories = module
.export_section()
.unwrap()
.entries()
.iter()
.filter(|entry| {
if let Internal::Memory(_) = entry.internal() {
true
} else {
false
}
});
assert_eq!(exported_memories.count(), 1);
}

#[test]
fn deserialize_corrupted_data() {
match deserialize(CORRUPTED).unwrap_err() {
VmError::StaticValidationErr { msg, .. } => {
assert!(msg.starts_with("Wasm bytecode could not be deserialized."))
}
err => panic!("Unexpected error: {:?}", err),
}
}

#[test]
fn check_wasm_passes_for_latest_contract() {
// this is our reference check, must pass
Expand Down Expand Up @@ -246,13 +184,13 @@ mod tests {
#[test]
fn check_wasm_memories_ok() {
let wasm = wat::parse_str("(module (memory 1))").unwrap();
check_wasm_memories(&deserialize(&wasm).unwrap()).unwrap()
check_wasm_memories(&deserialize_wasm(&wasm).unwrap()).unwrap()
}

#[test]
fn check_wasm_memories_no_memory() {
let wasm = wat::parse_str("(module)").unwrap();
match check_wasm_memories(&deserialize(&wasm).unwrap()) {
match check_wasm_memories(&deserialize_wasm(&wasm).unwrap()) {
Err(VmError::StaticValidationErr { msg, .. }) => {
assert!(msg.starts_with("Wasm contract doesn't have a memory section"));
}
Expand All @@ -276,7 +214,7 @@ mod tests {
))
.unwrap();

match check_wasm_memories(&deserialize(&wasm).unwrap()) {
match check_wasm_memories(&deserialize_wasm(&wasm).unwrap()) {
Err(VmError::StaticValidationErr { msg, .. }) => {
assert!(msg.starts_with("Wasm contract must contain exactly one memory"));
}
Expand All @@ -297,7 +235,7 @@ mod tests {
))
.unwrap();

match check_wasm_memories(&deserialize(&wasm).unwrap()) {
match check_wasm_memories(&deserialize_wasm(&wasm).unwrap()) {
Err(VmError::StaticValidationErr { msg, .. }) => {
assert!(msg.starts_with("Wasm contract must contain exactly one memory"));
}
Expand All @@ -309,10 +247,10 @@ mod tests {
#[test]
fn check_wasm_memories_initial_size() {
let wasm_ok = wat::parse_str("(module (memory 512))").unwrap();
check_wasm_memories(&deserialize(&wasm_ok).unwrap()).unwrap();
check_wasm_memories(&deserialize_wasm(&wasm_ok).unwrap()).unwrap();

let wasm_too_big = wat::parse_str("(module (memory 513))").unwrap();
match check_wasm_memories(&deserialize(&wasm_too_big).unwrap()) {
match check_wasm_memories(&deserialize_wasm(&wasm_too_big).unwrap()) {
Err(VmError::StaticValidationErr { msg, .. }) => {
assert!(msg.starts_with("Wasm contract memory's minimum must not exceed 512 pages"));
}
Expand All @@ -324,7 +262,7 @@ mod tests {
#[test]
fn check_wasm_memories_maximum_size() {
let wasm_max = wat::parse_str("(module (memory 1 5))").unwrap();
match check_wasm_memories(&deserialize(&wasm_max).unwrap()) {
match check_wasm_memories(&deserialize_wasm(&wasm_max).unwrap()) {
Err(VmError::StaticValidationErr { msg, .. }) => {
assert!(msg.starts_with("Wasm contract memory's maximum must be unset"));
}
Expand All @@ -346,7 +284,7 @@ mod tests {
"#;
let wasm_missing_exports = wat::parse_str(WAT_MISSING_EXPORTS).unwrap();

let module = deserialize(&wasm_missing_exports).unwrap();
let module = deserialize_wasm(&wasm_missing_exports).unwrap();
match check_wasm_exports(&module) {
Err(VmError::StaticValidationErr { msg, .. }) => {
assert!(msg.starts_with(
Expand All @@ -360,7 +298,7 @@ mod tests {

#[test]
fn check_wasm_exports_of_old_contract() {
let module = deserialize(CONTRACT_0_7).unwrap();
let module = deserialize_wasm(CONTRACT_0_7).unwrap();
match check_wasm_exports(&module) {
Err(VmError::StaticValidationErr { msg, .. }) => {
assert!(msg.starts_with(
Expand All @@ -384,7 +322,7 @@ mod tests {
)"#,
)
.unwrap();
check_wasm_imports(&deserialize(&wasm).unwrap(), SUPPORTED_IMPORTS).unwrap();
check_wasm_imports(&deserialize_wasm(&wasm).unwrap(), SUPPORTED_IMPORTS).unwrap();
}

#[test]
Expand Down Expand Up @@ -415,7 +353,7 @@ mod tests {
"env.debug",
"env.query_chain",
];
let result = check_wasm_imports(&deserialize(&wasm).unwrap(), supported_imports);
let result = check_wasm_imports(&deserialize_wasm(&wasm).unwrap(), supported_imports);
match result.unwrap_err() {
VmError::StaticValidationErr { msg, .. } => {
println!("{}", msg);
Expand All @@ -430,7 +368,7 @@ mod tests {

#[test]
fn check_wasm_imports_of_old_contract() {
let module = deserialize(CONTRACT_0_7).unwrap();
let module = deserialize_wasm(CONTRACT_0_7).unwrap();
let result = check_wasm_imports(&module, SUPPORTED_IMPORTS);
match result.unwrap_err() {
VmError::StaticValidationErr { msg, .. } => {
Expand All @@ -445,7 +383,7 @@ mod tests {
#[test]
fn check_wasm_imports_wrong_type() {
let wasm = wat::parse_str(r#"(module (import "env" "db_read" (memory 1 1)))"#).unwrap();
let result = check_wasm_imports(&deserialize(&wasm).unwrap(), SUPPORTED_IMPORTS);
let result = check_wasm_imports(&deserialize_wasm(&wasm).unwrap(), SUPPORTED_IMPORTS);
match result.unwrap_err() {
VmError::StaticValidationErr { msg, .. } => {
assert!(
Expand All @@ -471,7 +409,7 @@ mod tests {
)"#,
)
.unwrap();
let module = deserialize(&wasm).unwrap();
let module = deserialize_wasm(&wasm).unwrap();
let supported = HashSet::from_iter(
[
"water".to_string(),
Expand Down Expand Up @@ -500,7 +438,7 @@ mod tests {
)"#,
)
.unwrap();
let module = deserialize(&wasm).unwrap();
let module = deserialize_wasm(&wasm).unwrap();

// Support set 1
let supported = HashSet::from_iter(
Expand Down
Loading

0 comments on commit 31a5d0d

Please sign in to comment.