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

Add Cache::analyze #736

Merged
merged 8 commits into from
Jan 20, 2021
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: 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